CVE-2019-5736
前置知识
众所周知/proc
为内核管理进程的虚拟磁盘,/proc/[pid]
中存放着pid
号进程的相关信息,/proc/self
中存放着启动当前进程的相关信息,pwn
中常用的/proc/self/mem
和/proc/self/maps
就位于其中,此CVE
中涉及到的两个文件/文件夹为:
/proc/self/fd
,文件夹中存放着当前进程打开的fd
所指向的文件。
/proc/self/exe
,此符号链接指向启动当前进程的可执行文件。
runc
为docker
底层在运行启动容器时运行的程序,老版本为docker-runc
,后被单独提出为runC
,也就是说当我们在宿主机上运行docker exec [containerid] cmd
时底层会运行docker-runc
。
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /bin/ls -al /proc/self/exe
命令的/proc/self/exe
指向ld-linux-x86-64.so.2
,而不是/bin/ls
,类似的docker-runc xxx /bin/sh
的/proc/self/exe
指向docker-runc
。
O_PATH
标准位为获取一个虚拟句柄,用于保存作用,不具有权限,不可对其直接进行读写。O_PATH
对于普通文件的一种用法是提供一种等价于POSIX.1
中O_EXEC
标志的功能,这允许我们在只有可执行权限却没有可读权限时打开一个文件,然后执行这个文件。
搭建环境
thinkycx
的快速搭建环境脚本,centos7/ubuntu16.04
:
https://gist.githubusercontent.com/thinkycx/e2c9090f035d7b09156077903d6afa51/raw/
漏洞分析
此漏洞的是DragonSector
团队在35C3 CTF
之后的后续研究工作,所以其核心思路为研究当一个新进程加入到一个已有的NameSpace(比如 docker exec)
中会发生什么事情,是否可以通过新加入的进程来访问宿主机资源。
然后团队开始着重研究/proc
文件系统,从而发现了漏洞。
不同于以前使用
libcontainer
管理容器实例。Docker目前使用一个独立的子项目runc
来管理所有的容器实例。在容器管理过程中,一个常见的操作是宿主机需要在容器中启动一个新的进程。包括容器启动时的init
进程也需要由宿主机启动。为了实现该操作,一般由宿主机fork
一个新进程,由该进程使用setns
系统调用进入容器的namespace
中。然后再调用exec
在容器中执行需要的进程。该操作一般称之为进入容器(nsenter)。在runc
项目中,虽然大部分代码都是go
语言编写的,但是进入容器部分代码却是使用C语言编写的(runc/libcontainer/nsenter/nsexec.c)。漏洞就这部分代码中,在
runc
进程进入容器时,没有对自身ELF
文件进行克隆拷贝。这就导致runc
在进入容器之后,在执行exec
之前,其/proc/{PID}/exe
这个软链接指向了宿主机runc
程序。由于docker默认不启用User Namespace,这导致容器内进程可以读写runc
程序文件。攻击者可以替换runc
程序,在宿主机下一次使用docker的时候就可以获得任意代码执行的机会。
Let’s go over the vulnerability overview given by the runC team:
The vulnerability allows a malicious container to (with minimal user interaction) overwrite the host runc binary and thus gain root-level code execution on the host. The level of user interaction is being able to run any command … as root within a container in either of these contexts:
Creating a new container using an attacker-controlled image.
Attaching (docker exec) into an existing container which the attacker had previous write access to.
Those two scenarios might seem different, but both require runC to spin up a new process in a container and are implemented similarly. In both cases, runC is tasked with running a user-defined binary in the container. In Docker, this binary is either the image’s entry point when starting a new container, or docker exec’s argument when attaching to an existing container.
When this user binary is run, it must already be confined and restricted inside the container, or it can jeopardize the host. In order to accomplish that, runC creates a ‘runC init’ subprocess which places all needed restrictions on itself (such as entering or setting up namespaces) and effectively places itself in the container. Then, the runC init process, now in the container, calls the execve syscall to overwrite itself with the user requested binary.
This is the method used by runC both for creating new containers and for attaching a process to an existing container.
The researchers who revealed the vulnerability discovered that an attacker can trick runC into executing itself by asking it to run /proc/self/exe, which is a symbolic link to the runC binary on the host.
An attacker with root access in the container can then use /proc/[runc-pid]/exe as a reference to the runC binary on the host and overwrite it. Root access in the container is required to perform this attack as the runC binary is owned by root.
The next time runC is executed, the attacker will achieve code execution on the host. Since runC is normally run as root (e.g. by the Docker daemon), the attacker will gain root access on the host.
为了加深理解,我写了两个简易的demo
测试,test
相当于宿主机进程,test
的子进程等价于进入docker-runc
的namespace
的runc-init
进程,test1
相当于容器中的exp
:
先运行test
,然后获得其子进程pid
后,运行test1
,然后将test
进程结束(按两下回车),然后将test1
进程结束,然后可以发现test
文件内容被修改为payload
。
1 | /* |
1 | /* |
漏洞利用
exp1,C语言版 => https://github.com/feexd/pocs/blob/master/CVE-2019-5736/exploit.c:
1 |
|
exp2,go版 => https://github.com/Frichetten/CVE-2019-5736-PoC:
1 | package main |
exp
流程为(go
版):
docker
中运行exp
,exp
进程先将/bin/sh
改为#!/proc/self/exe
为后续使用做准备,再循环搜索runc-init
子进程。- 宿主机中运行
docker exec [container_id] /bin/sh
,此时宿主机底层将运行docker-runc
,相应的子进程runc-init
会进入docker
的namespace
中。 docker
中exp
进程搜索到runc-init
进程,获取其pid
,打开其/proc/pid/exe
,等价为获取到docker-runc
文件的句柄将其暂存到/proc/self/fd
中,无法直接对/proc/pid/exe
的句柄写入,因为无法写入正在执行的可执行文件。- 待宿主机中
docker-runc
进程运行结束,docker
中exp
进程可以打开/proc/self/fd/3
,再次获取到宿主机docker-runc
文件的句柄,对其进行写入,写入我们想运行的任意command
。 docker
中继续运行宿主机第二步传的命令,运行/bin/sh
,因为此时的/bin/sh
已经被我们更改为#!/proc/self/exe
,所以会将/proc/self/exe
作为解释器执行,/proc/self/exe
指向docker-runc
文件,所以执行了我们第四步传入的command
,完成攻击。
参考
https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html
https://unit42.paloaltonetworks.com/breaking-docker-via-runc-explaining-cve-2019-5736/
https://github.com/q3k/cve-2019-5736-poc
https://github.com/Frichetten/CVE-2019-5736-PoC