CVE-2019-5736 Docker逃逸

前言: 最近公开了一个 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:~# docker info | grep "runc"
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 漏洞利用

  1. 容器内想办法获取 Runc PID。
  2. 得到 PID 后,获取文件描述符
  3. 对 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;

/* Check that we're not self-cloned, and if we are then bail. */
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
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
/* Need to re-open "memfd" as read-only to avoid execve(2) giving -EXTBUSY. */
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