前言: 最近公开了一个 Docker逃逸的漏洞,在35c3 ctf结束后,由 dragonsecto 发现。CVE-2019-5736: Escape from Docker and Kubernetes containers to root on host就此自己稍微分析了一下原理。
0x01 RunC
众所周知,RunC 是一个轻量级的工具,它是用来运行容器的,只用来做这一件事,并且这一件事要做好。我们可以认为它就是个命令行小工具,可以不用通过 docker 引擎,直接运行容器。事实上,runC 是标准化的产物,它根据 OCI 标准来创建和运行容器。而 OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。
换一句话说,其实 Docker 在管理容器的时候,其实底层就是跑的RunC
1 2 3 4 5
| root@VM-118-78-ubuntu:~ Runtimes: runc Default Runtime: runc runc version: N/A (expected: 54296cf40ad8143b62dbcaa1d90e520a2136ddfe) WARNING: No swap limit support
|
从图中就可以得知,runc 和docker 的命令执行效果其实是差不多的。
0x02 PID NameSpace
PID Namespace 隔离进程pid之后,ns中的进程无法发现外界的进程。而外部ns中进程可以发现ns中进程。
0x03 漏洞利用点
另外一个值得注意的是 /proc/self/exe
可以发现 proc/pid/exc
正常是会被指向所运行文件。而这个漏洞的漏洞利用点正在于此,如果使用此时用 docker exec binfile
。此时如果能在容器内,拿到 runc 的pid 从而获取 runc 的符号链接。
那么我们就能通过覆盖 runc 为恶意程序来达到 Docker 逃逸的目的。
0x04 漏洞利用
- 容器内想办法获取 Runc PID。
- 得到 PID 后,获取文件描述符
- 对 fd 进行写操作,覆盖原有 runc
- 获取 RunC的PID,我们知道 容器内的PID 是通过namespace 特殊隔离的,通常而言,如果此时有进程 A ,进程 A 的PID为 233,那么下一步我们运行个进程 B,那么此时进程 B 的PID理应为 234
- 获取文件操作符
1 2 3
| if 'runc' exe_name: fp = open('/proc/%d/exe' % pid,'r') fd = fp.fileno()
|
- 然后就可以 通过打开 fd 的形式,对 runc 文件进行操作。
演示效果如下:
请全屏观看!!!
0x05 补丁分析
merge branch ‘cve-2019-5736’
可以看到 添加了一个 ensure_cloned_binary 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int ensure_cloned_binary(void) { int execfd; char **argv = NULL, **envp = NULL;
int cloned = is_self_cloned(); if (cloned > 0 || cloned == -ENOTRECOVERABLE) return cloned;
if (fetchve(&argv, &envp) < 0) return -EINVAL;
execfd = clone_binary(); if (execfd < 0) return -EIO;
fexecve(execfd, argv, envp); return -ENOEXEC; }
|
首先判断 exe 是否被clone
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| static int is_self_cloned(void) { int fd, ret, is_cloned = 0;
fd = open("/proc/self/exe", O_RDONLY|O_CLOEXEC); if (fd < 0) return -ENOTRECOVERABLE;
#ifdef HAVE_MEMFD_CREATE ret = fcntl(fd, F_GET_SEALS); is_cloned = (ret == RUNC_MEMFD_SEALS); #else struct stat statbuf = {0}; ret = fstat(fd, &statbuf); if (ret >= 0) is_cloned = (statbuf.st_nlink == 0); #endif close(fd); return is_cloned; }
|
如果否,则执行 clone_binary 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| static int clone_binary(void) { int binfd, memfd; ssize_t sent = 0;
#ifdef HAVE_MEMFD_CREATE memfd = memfd_create(RUNC_MEMFD_COMMENT, MFD_CLOEXEC | MFD_ALLOW_SEALING); #else memfd = open("/tmp", O_TMPFILE | O_EXCL | O_RDWR | O_CLOEXEC, 0711); #endif if (memfd < 0) return -ENOTRECOVERABLE;
binfd = open("/proc/self/exe", O_RDONLY | O_CLOEXEC); if (binfd < 0) goto error;
sent = sendfile(memfd, binfd, NULL, RUNC_SENDFILE_MAX); close(binfd); if (sent < 0) goto error;
#ifdef HAVE_MEMFD_CREATE int err = fcntl(memfd, F_ADD_SEALS, RUNC_MEMFD_SEALS); if (err < 0) goto error; #else int newfd; char *fdpath = NULL;
if (asprintf(&fdpath, "/proc/self/fd/%d", memfd) < 0) goto error; newfd = open(fdpath, O_RDONLY | O_CLOEXEC); free(fdpath); if (newfd < 0) goto error;
close(memfd); memfd = newfd; #endif return memfd;
error: close(memfd); return -EIO; }
|
return 一个新的 fd
0x06 参考链接
oss-security
CVE-2019-36 PoC
CVE-2019-5736 docker image and exploit
CVE-2019-5736: Escape from Docker and Kubernetes containers to root on host
文章来源: https://bestwing.me/CVE-2019-5736-Docker-escape.html
如有侵权请联系:admin#unsafe.sh