35C3 CTF是在第35届混沌通讯大会期间,由知名CTF战队Eat, Sleep, Pwn, Repeat于德国莱比锡举办的一场CTF比赛。比赛中有一道基于Linux命名空间机制的沙盒逃逸题目。赛后,获得第三名的波兰强队Dragon Sector发现该题目所设沙盒在原理上与docker exec命令所依赖的runc(一种容器运行时)十分相似,遂基于题目经验对runc进行漏洞挖掘,成功发现一个能够覆盖宿主机runc程序的容器逃逸漏洞。该漏洞于2019年2月11日通过邮件列表披露,分配编号CVE-2019-5736。
本文将对该CTF题目和CVE-2019-5736作完整分析,将整个过程串联起来,以期形成对容器底层技术和攻击面更深刻的认识,并学习感受其中的思维方式。
有些鸟是不能关在笼子里的,他们的羽毛太漂亮了。
35C3 CTF是在第35届混沌通讯大会期间,由知名CTF战队Eat, Sleep, Pwn, Repeat于德国莱比锡举办的一场CTF比赛。比赛中有一道基于Linux命名空间机制[10]的沙盒逃逸题目(类别为Pwn)。赛后,获得第三名的波兰强队Dragon Sector发现该题目所设沙盒在原理上与docker exec命令所依赖的runc(一种容器运行时)十分相似,遂基于题目经验对runc进行漏洞挖掘,成功发现一个能够覆盖宿主机runc程序的容器逃逸漏洞。该漏洞于2019年2月11日通过邮件列表披露,分配编号CVE-2019-5736。
自漏洞披露以来,网络上陆续有一些分析文章出现。其中不乏洞见之作,然而部分细节的缺失使得它们对于逻辑严谨但缺乏相关背景知识的读者来说并不十分友好。一方面,本文期望能够给出一个内容翔实、逻辑完整的漏洞分析;另一方面,如前所述的整个事件是一个从模拟场景到真实场景、从CTF题目到实际漏洞的极好示例——笔者希望借助对Dragon Sector从Pwn到发现漏洞的历程重现,形成对容器底层技术和攻击面更深刻的认识,并学习感受其中的思维方式。
本文涉及到大量容器和Linux系统相关的背景知识,限于篇幅无法一一进行讲解。部分缺乏这些背景知识的读者可能会有困惑。建议采用“深度优先搜索“的方式阅读文章,即遇到陌生概念时先去寻找资料把这个概念大致弄明白,再回来继续阅读。希望通过这样的阅读,您能有所收获。
后文结构如下:首先对35C3 CTF题目进行分析,其次是CVE-2019-5736,最后对整个分析过程作总结。
文中如有不当之处,还请读者朋友指教。
1题目概述
Here is another linux user namespaces challenge by popular demand.
For security reasons, this sandbox needs to run as root. If you can break out of the sandbox, there’s a flag in /, but even then you might not be able to read it :).
The files are here: https://35c3ctf.ccc.ac/uploads/namespaces-a4b1ac039830f7c430660bc155dd2099.tar Service running at: nc 35.246.140.24 1
Hints:
• You’ll need to create your own user namespace for the intended solution.
从题面上我们知道,这是一道与Linux命名空间有关的沙盒题目,任务是逃出沙盒,拿到flag。
下载文件包并解压,得到两个文件:一个Dockerfile和一个名为namespaces的64位Linux可执行文件。
其中,Dockerfile内容如下:
1FROM tsuro/nsjail
2COPY challenge/namespaces /home/user/chal
3#COPY tmpflag /flag
4CMD /bin/sh -c "/usr/bin/setup_cgroups.sh && cp /flag /tmp/flag && chmod 400 /tmp/flag && chown user /tmp/flag && su user -c '/usr/bin/nsjail -Ml --port 1337 --chroot / -R /tmp/flag:/flag -T /tmp --proc_rw -U 0:1000:1 -U 1:100000:1 -G 0:1000:1 -G 1:100000:1 --keep_caps --cgroup_mem_max 209715200 --cgroup_pids_max 100 --cgroup_cpu_ms_per_sec 100 --rlimit_as max --rlimit_cpu max --rlimit_nofile max --rlimit_nproc max -- /usr/bin/stdbuf -i0 -o0 -e0 /usr/bin/maybe_pow.sh /home/user/chal'"
注:本文成稿时似乎35C3 CTF官网已经关闭,如需本题目附件,可关注“绿盟科技研究通讯”公众号,回复35c3ctf进行下载。附件相关权利为35C3 CTF主办方所有,如有不当,请联系我们删除。
2漏洞定位与分析
NsJail[4]是由Google开源的一款进程隔离工具,常用于CTF比赛题目的部署。它的参数有很多,感兴趣者可以自行到官网了解。
Dockerfile中最后以user用户身份运行NsJail,创建了一个隔离环境:
1/usr/bin/nsjail -Ml --port 1337 --chroot / -R /tmp/flag:/flag -T /tmp --proc_rw -U 0:1000:1 -U 1:100000:1 -G 0:1000:1 -G 1:100000:1 --keep_caps --cgroup_mem_max 209715200 --cgroup_pids_max 100 --cgroup_cpu_ms_per_sec 100 --rlimit_as max --rlimit_cpu max --rlimit_nofile max --rlimit_nproc max -- /usr/bin/stdbuf -i0 -o0 -e0 /usr/bin/maybe_pow.sh /home/user/chal
1. 监听在1337端口(-Ml --port 1337);
2. 没有切换根目录(--chroot /);
3. 将/tmp/flag以只读方式绑定挂载到/flag,并在/tmp处挂载一个tmpfs(-R /tmp/flag:/flag -T /tmp);
4. 将procfs挂载为可读写模式(/proc/_rw);
5. UID/GID:隔离环境内的0和1分别映射为环境外的1000和100000(-U 0:1000:1 -U 1:100000:1 -G 0:1000:1 -G 1:100000:1);
6. 保留所有capabilities[5](--keep_caps)。
其他参数对于攻克挑战来说无关紧要。最后,NsJail将运行/home/user/chal,也就是前面提到的namespaces二进制文件。
分析到这里,我们可以确定的是,在隔离环境内部,通过/tmp/flag路径已经不能直接拿到flag,因为它被新的tmpfs遮盖;通过/flag路径能够拿到flag,虽然一开始我们不知道它的权限和所有者,但现在挂载在这里的其实是原先的/tmp/flag,属于user用户,而当前的隔离环境恰恰是以user身份运行。
所以,如果能利用后面的namespaces程序在这个隔离空间内获得user身份的代码执行机会,就能拿到flag。
注:这个Dockerfile可能会给一些朋友造成误解。事实上,Docker本身和NsJail只是用来部署题目的工具,并非要逃逸的沙盒。后面将要分析的namespaces程序才是需要突破的有缺陷沙盒。
虽然对于沙盒类题目来说不是很必要,但还是常规操作看一下namespaces的文件类型:
1rambo@matrix:~/namespaces$ file namespaces
2namespaces: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=9e6a81c671a2d46fc420b7cd0851c482c48ee53a, not stripped
这4步讲完,您可能会觉得有点绕。但是,一方面,这个程序的代码逻辑本身真的非常简单,推荐自己动手逆向看看;另一方面,将这一系列的操作和容器类比来看,我们会发现它们很相像:上述第3步创建沙盒并启动一个init进程,这与容器的创建和启动方式大体相同,第4步则模拟了docker exec,即容器内执行命令的操作。也难怪Dragon Sector在赛后会跃跃欲试去看Docker有没有类似的漏洞。当然,这是后话,何况CVE-2019-5736的成因其实与本题并不相同。我们还是回到当前题目的分析中来。
经过上述讲解,或许有读者即使还没有发现漏洞所在,也已经发现了异常之处——第4步run_elf分支中“依次加入命名空间”的步骤竟然漏掉很重要的一个——net命名空间!
进程没有加入所在沙盒的net命名空间有什么影响呢?
这意味着,它能够直接看到宿主的网络接口。在题目环境里,就是我们借助run_elf运行的程序能够直接看到/home/user/chal视角下的网络接口,而非它所在沙盒内部的。因此,不同沙盒内部通过run_elf运行的程序能够互相通信。
那么,如何借助这一特点完成沙盒逃逸呢?
Linux系统中有一类特殊的文件操作API,它们的名称以at结尾,如openat、unlinkat和symlinkat等。它们与不带at的函数功能相同,只是通过一个文件描述符加基于该文件描述符对应文件的相对路径来获得最终的文件路径,而非传统上直接由调用者给出字符串参数指定。前面三个函数的定义如下:
1int openat(int dirfd, const char *pathname, int flags);
2int unlinkat(int dirfd, const char *pathname, int flags);
3int symlinkat(const char *target, int newdirfd, const char *linkpath);
事实上,这个思路是可行的。参考文档[6]可知,我们可以借助unix socket以“辅助消息”(Ancillary messages)的方式在指定类型为SCM_RIGHTS时发送和接收文件描述符;然而,各个沙盒进程的mnt命名空间互相隔离,不同沙盒进程无法通过打开同一unix socket文件的方式实现通信。
同样由文档[6]可知,Linux支持一类独立于文件系统的抽象命名空间(Abstract namespace),我们能够将unix socket绑定到抽象命名空间内的一个名称上,而非在本地文件系统上创建一个socket文件,这样一来,不同沙盒中run_elf的进程就能够通过同一个名称找到对应unix socket,从而实现文件描述符的传递。
我们注意到,沙盒本身是以user身份运行的,只是分别在start_box和run_elf分支经过降权(setr)罢了。如果能够阻止降权,就能够获得user权限。从2.2.2节可以知道,run_elf分支在降权前执行了依次加入沙盒命名空间的操作。如果能够在这些步骤后不执行降权操作,就不会降权。进一步地,如果能够在这些步骤后直接执行我们想要执行的代码,譬如读取/flag,就实现了以user身份代码执行的目的。
如何实现呢?
如果我们能够ptrace到一个run_elf进程上,就能够向其中注入代码,而这要求ptrace进程与被调试的run_elf进程在同一个pid命名空间内。回顾前面的内容,run_elf将依次打开并加入/proc/[初始进程PID]/ns/下的user、 mnt、 pid、 uts、 ipc和cgroup命名空间。设想这样一种情况:假如我们创建一个沙盒,其中的init进程fork一个子进程,然后将/tmp/xxx目录绑定挂载到/proc/[init进程PID]/ns,接着在这个目录下创建符号链接,将各个命名空间链接到init进程fork的子进程对应的/proc/[子进程PID]/ns目录下,那么当一个run_elf进程加入沙盒init进程的mnt命名空间后,它将看到被上述操作修改过的/proc,接着它加入的pid命名空间实际上属于init的子进程。这样一来,init子进程就能够在这个pid命名空间下借助ptrace向未降权的run_elf进程注入代码并执行了。为了提高成功率,我们甚至可以将init进程的uts命名空间设置为一个管道,当run_elf进程尝试加入这个命名空间时,它将被阻塞住,从而阻止了降权操作。
至此,似乎我们达到了以user身份代码执行的目的。然而,上面的思路还是存在问题。
2.2.2节一开始提到所有沙盒所在目录/tmp/chroots的权限为777,而2.2.3.1节中我们已经能够通过传递文件描述符来让一个run_elf进程访问到chroot外的文件系统。综合两者来看,我们有以下逃逸chroot的方案:
3漏洞利用
我们先在本地搭建起漏洞环境,将题目运行起来:
接着运行漏洞利用代码,效果如下图所示(略去了前面的交互过程):
至此,关于这道题目的讲解到这里告一段落。总结一下,上面的关键问题有两个:
1. 外来进程并没有完全加入沙盒所有命名空间(net命名空间);
2. 外来进程是依次加入沙盒命名空间的,尤其是在加入pid命名空间时,由于其特性(修改pid命名空间只在子进程生效),直接fork出子进程,这给了我们竞态攻击的机会。
1漏洞概述
那么,容器运行时在容器内部执行命令时是否也存在上面提到的“依次加入命名空间”的问题呢?如果是,那么它就很可能面临同样的缺陷。以runc为例(后文均以runc为例进行说明):runc exec时先加入user和pid命名空间,接着fork出子进程,再加入其他命名空间。如果恶意进程在容器内检测到runc加入了自己的pid命名空间时,直接调用ptrace向runc进程注入恶意代码,就能够实现容器外代码执行。
很遗憾,一方面,runc是在加入了所有命名空间后才fork出子进程的;另一方面,docker的默认安全配置不允许容器内部执行和命名空间相关的系统调用。这个思路行不通。
后来,他们的思路转向proc伪文件系统[11],成功发现了漏洞。下一节,我们将对漏洞成因进行分析。
2 漏洞分析
执行过程大体是这样的:runc启动,加入到容器的命名空间,接着以自身(/proc/self/exe,后面会解释)为范本启动一个子进程,最后通过exec系统调用执行用户指定的二进制程序。
这个过程看起来似乎没有问题,相关风险点我们在3.1节也已经分析过了。现在,我们需要让另一个角色出场——proc伪文件系统,即/proc。关于这个概念,Linux文档[11]已经给出了详尽的说明,这里我们主要关注/proc下的两类文件:
1. /proc/[PID]/exe:它是一种特殊的符号链接,又被称为magic links(为什么将这类符号链接叫做magic links呢?请参考附录内容,这一点对当前漏洞的形成至关重要),指向进程自身对应的本地程序文件(例如我们执行ls,/proc/[ls-PID]/exe就指向/bin/ls);
2. /proc/[PID]/fd/:这个目录下包含了进程打开的所有文件描述符。
/proc/[PID]/exe的特殊之处在于,如果你去打开这个文件,在权限检查通过的情况下,内核将直接返回给你一个指向该文件的描述符(file descriptor),而非按照传统的打开方式去做路径解析和文件查找。这样一来,它实际上绕过了mnt命名空间及chroot对一个进程能够访问到的文件路径的限制。
那么,设想这样一种情况:在runc exec加入到容器的命名空间之后,容器内进程已经能够通过内部/proc观察到它,此时如果打开/proc/[runc-PID]/exe并写入一些内容,就能够实现将宿主机上的runc二进制程序覆盖掉!这样一来,下一次用户调用runc去执行命令时,实际执行的将是攻击者放置的指令。
1. 需要具有容器内部root权限;
2. Linux不允许修改正在运行进程对应的本地二进制文件。
事实上,限制1经常不存在,很多容器服务开放给用户的仍然是root权限;而限制2是可以克服的,后面一节会讲到具体的利用方式。
可以看到这个漏洞的成因比上面的CTF题目简单许多(虽然要完全理解还需要补充很多背景知识)。
3漏洞利用
相对于CTF题目来说,这个漏洞的利用代码[9]也比较简单。其步骤可归纳如下:
我们先在本地搭建起漏洞环境(下图中给出了docker和runc的版本号供参照),然后运行一个容器,在容器中模仿攻击者执行/poc程序,该程序在覆盖容器内/bin/sh为#!/proc/self/exe后等待runc的出现。具体过程如下图所示(图中下方“找到PID为28的进程并获得文件描述符”是宿主机上受害者执行docker exec操作之后才触发的):
容器内的/poc程序运行后,我们在容器外的宿主机上模仿受害者使用docker exec命令执行容器内/bin/sh打开shell的场景。触发漏洞后,一如预期,并没有交互式shell打开,相反,/tmp下已经出现攻击者写入的hello,host,具体过程如下图所示:
以上过程表明,借助这个漏洞,容器内进程具备在容器外执行代码的能力。
4漏洞修复
这样一来,在Linux匿名机制的代码实现确保其效果的前提下,容器内的恶意进程就无法通过前文所述/proc/[PID]/exe的方式触及到宿主机上的runc二进制程序。
然而,这种修复方式有一个副作用:增大了容器的内存负担。社区已经有人证实这一点并在Github上反映情况[13]。
最直接的感受可能是,跨命名空间的操作很容易引入漏洞。加入新的命名空间很容易,然而新的命名空间是否可信?其中具有CAP_SYS_ADMIN权限的进程是否可控?这些是加入前要考虑清楚的问题。
我们继续。Linux命名空间的概念最早来源于贝尔实验室的Plan 9分布式系统项目[14],第一个出现在Linux内核中的是mnt命名空间,始于内核版本2.4.19,而目前为止最后一个加入的user命名空间已经是内核版本3.8了[15];另一方面,proc伪文件系统同样由来已久。这两者分别单独拿出来时,似乎并没有什么问题,即使像/proc下的magic links也不会引起很大麻烦。但放在一起后,结果我们已经看到了。
成熟复杂系统(譬如Linux)的魅力在于其能够提供强大的功能和机制,而问题则往往出现在这些功能与机制同时或交替生效的场景中。有时我们会把这类问题称为逻辑漏洞。当然,这类漏洞是可以修复的,在一定程度上也是可以规避的。另外,从上面介绍的CVE-2019-5736漏洞利用代码我们能够感受到,针对逻辑漏洞的利用可以是简单甚至优雅的,但最初把各种机制放在一起检查到底有没有漏洞、有什么漏洞却并不容易。
在云计算世界,我们尤其擅长将各种基础机制打包起来,创造出新的事物,这种新事物也许能够极大地提高生产力,甚至促进产业变革——容器便是典例。然而,结合前文所述,这也意味着以往不曾出现过的机制交叠带来的逻辑漏洞或许会在云环境陆续产生。例如,在今年的欧洲开源峰会(Open Source Summit Europe 2019)上,有议题展示了“命名空间”与“符号链接”两个概念放在一起出现的一系列问题[16],感兴趣的读者可以关注一下。
最后,引用道哥的一句话作结:
建设更安全的互联网。
我们知道,/proc目录下有许多符号链接,例如/proc/[PID]/exe和/proc/[PID]/cwd。然而,它们并非真正的符号链接,或者说,它们是一种特殊的符号链接,叫做magic links。首先,我们可以借助一个小实验来观察它们与普通符号链接的不同:
如上图,我们创建了一个普通符号链接,可以看到它的文件长度为目标文件名的长度,即6;但/proc/self/exe的长度却是0,而非其所指目标文件/bin/ls名称的长度。这个差异从一定程度上说明了/proc下符号链接的特殊性。
当然,将它们称作magic links的原因并非这么简单。其中很重要的一点是,当进程去操作一个这样的符号链接时,例如“打开”操作,Linux内核不会按照普通符号链接处理方式在文件系统上做路径解析,而是会直接调用专属的处理函数并返回对应文件的文件描述符。
到目前为止,magic links的概念并没有被很好地文档化,Aleksa Sarai在对manpage的修改[8]中给出了一些有用的说明,笔者将它们摘录到这里,供大家参考:
There is a special class of symlink-like objects known as "magic-links" which can be found in certain pseudo-filesystems such as proc (5) (examples include /proc/[pid]/exe and /proc/[pid]/fd/ .)
Unlike normal symlinks, magic-links are not resolved through pathname-expansion, but instead act as direct references to the kernel's own representation of a file handle. As such, these magic-links allow users to access files which cannot be referenced with normal paths (such as unlinked files still referenced by a running program.) Because they can bypass ordinary mount_namespaces (7)-based restrictions, magic-links have been used as attack vectors in various exploits.
As such (since Linux 5.FOO), there are additional restrictions placed on the re-opening of magic-links (see path_resolution (7) for more details.)
其中最重要的一句话是:
因此,magic links是“不走寻常路”的。
也正因为这个概念没有很好地文档化,也许有的读者会觉得“口说无凭”。这里留一个小题目给感兴趣的读者:在Linux内核源码中找到操作magic links的具体逻辑流程。这样做的好处有三:一方面,为magic links的特殊处理提供了最有力的证据;另一方面,能够锻炼从庞杂信息中寻找线索解决问题的能力;最后,能够加深对Linux内核文件处理流程的认识。
下面给出一些提示:
1. 先不要去最新版本的源码中找。如上面摘录内容所述,5.x版本的代码可能增加了新的检查项目,提高了复杂度(笔者研究时使用的是4.14.151版代码);
2. 可以以系统调用为探索起点。例如,从open系统调用开始,一步步向后深入;
3. fs/proc是最重要的目录。
在研究过程中,笔者曾就几个技术细节问题向参考文献条目2、3的作者Yuval Avrahami和LevitatingLion请教,在此向两位安全研究人员表示感谢。
[1]. CVE-2019-5736: Escape from Docker and Kubernetes containers to root on host; https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html
[2]. Breaking out of Docker via runC – Explaining CVE-2019-5736;https://www.twistlock.com/labs-blog/breaking-docker-via-runc-explaining-cve-2019-5736/
[3]. Escaping a Broken Container - 'namespaces' from 35C3 CTF;http://blog.perfect.blue/namespaces-35c3ctf
[4]. NsJail;https://google.github.io/nsjail/
[5]. Linux Programmer's Manual: capabilities - overview of Linux capabilities;http://man7.org/linux/man-pages/man7/capabilities.7.html
[6]. Linux Programmer's Manual: unix - sockets for local interprocess communication;http://man7.org/linux/man-pages/man7/unix.7.html
[7]. ctf-writeups/35c3ctf/pwn_namespaces;https://github.com/LevitatingLion/ctf-writeups/tree/master/35c3ctf/pwn_namespaces
[8]. [PATCH RFC 1/3] symlink.7: document magic-links more completely;https://lkml.org/lkml/2019/10/3/507
[9]. Frichetten/CVE-2019-5736-PoC;https://github.com/Frichetten/CVE-2019-5736-PoC
[10]. Linux Programmer's Manual: namespaces - overview of Linux namespaces;http://man7.org/linux/man-pages/man7/namespaces.7.html
[11]. Linux Programmer's Manual: proc - process information pseudo-filesystem;http://man7.org/linux/man-pages/man2/memfd_create.2.html
[12]. Linux Programmer's Manual: memfd_create - create an anonymous file;http://man7.org/linux/man-pages/man2/memfd_create.2.html
[13]. CVE-2019-5736: Runc uses more memory during start up after the fix;https://github.com/opencontainers/runc/issues/1980
[14]. The Use of Name Spaces in Plan 9;http://9p.io/sys/doc/names.html
[15]. 《自己动手写Docker》,第2章
[16]. In-and-out - Security of Copying to and from Live Containers - Ariel Zelivansky & Yuval Avrahami, Twistlock; https://osseu19.sched.com/event/TLC4/in-and-out-security-of-copying-to-and-from-live-containers-ariel-zelivansky-yuval-avrahami-twistlock
[17]. containerd; https://containerd.io/
星云实验室专注于云计算安全、解决方案研究与虚拟化网络安全问题研究。基于IaaS环境的安全防护,利用SDN/NFV等新技术和新理念,提出了软件定义安全的云安全防护体系。承担并完成多个国家、省、市以及行业重点单位创新研究课题,已成功孵化落地绿盟科技云安全解决方案
往期回顾
本公众号原创文章仅代表作者观点,不代表绿盟科技立场。所有原创内容版权均属绿盟科技研究通讯。未经授权,严禁任何媒体以及微信公众号复制、转载、摘编或以其他方式使用,转载须注明来自绿盟科技研究通讯并附上本文链接。
关于我们
绿盟科技研究通讯由绿盟科技创新中心负责运营,绿盟科技创新中心是绿盟科技的前沿技术研究部门。包括云安全实验室、安全大数据分析实验室和物联网安全实验室。团队成员由来自清华、北大、哈工大、中科院、北邮等多所重点院校的博士和硕士组成。
绿盟科技创新中心作为“中关村科技园区海淀园博士后工作站分站”的重要培养单位之一,与清华大学进行博士后联合培养,科研成果已涵盖各类国家课题项目、国家专利、国家标准、高水平学术论文、出版专业书籍等。
我们持续探索信息安全领域的前沿学术方向,从实践出发,结合公司资源和先进技术,实现概念级的原型系统,进而交付产品线孵化产品并创造巨大的经济价值。
长按上方二维码,即可关注我