poc地址:https://github.com/twistlock/RunC-CVE-2019-5736

今年2月的时候,国外披露了一个docker容器逃逸的漏洞(CVE-2019-5736),这个漏洞允许攻击者通过覆盖宿主机上的runc二进制文件的方式执行代码,而因为docker一般运行在root模式下的原因,这个漏洞可以让攻击者直接获取root的权限。

runc

在docker的使用中,runc的作用是在容器中运行用户所定义的二进制文件,即images镜像的入口文件(Dockerfile里的cmd及entrypoint),或者在现有容器内执行(docker exec)的文件。在运行runc文件的时候,它必须被限制在容器中,不然用户所定义的二进制文件就有可能影响到宿主机。而为了实现这一点,runc会创建一个子进程”runc init”,并将所有需要的限制都放在它身上。最后,runc init进程会调用execve函数,运行用户所定义的二进制文件。还有则是,在停止容器的时候会运行runc delete,同样会执行runc。

攻击方式

  • 在受感染的Docker容器上从主机运行”docker exec”。
  • 启动恶意Docker镜像。

听起来是不是特别给力?是不是?然而这个漏洞的影响范围出乎意料的小。

漏洞影响

  • Ubuntu:runc 1.0.0~rc4+dfsg1-6ubuntu0.18.10.1之前版本
  • Debian:runc 0.1.1+dfsg1-2 之前版本
  • RedHat Enterprise Linux: docker 1.13.1-91.git07f3374.el7之前版本
  • Amazon Linux:docker 18.06.1ce-7.25.amzn1.x86_64之前版本
  • CoreOS:2051.0.0之前版本
  • Kops Debian 所有版本(正在修复)
  • Docker:18.09.2之前版本

也就是说这个漏洞,需要系统、docker、runc三个版本都符合才能造成影响。而我花了三四天的时间,测试了多个系统和多个docker版本,包括其他师傅提到的可以成功的环境,却没有一个能够成功的。

漏洞分析

在docker运行的时候,宿主机通过runc对docker容器进行操作,而在这个过程中却没有做好访问限制,导致runc可以通过/proc/self/exe符号链接来访问到宿主机上的runc文件。攻击者可以让runc运行/proc/self/exe符号链接来欺骗它自己执行自己,然后如果攻击者在容器内具有root权限(因为runc的所有用户为root),就可以通过/proc/[runc-pid]/exe这个符号链接对宿主机上的runc文件进行覆盖重写。

Linux中的”#!”被称为Shebang,它被用于指定脚本的解释器,我们经常可以在Shell脚本或者Python脚本中看到它的身影。而当Linux遇到Shebang的时候,它会去运行解释器。在docker的使用中用户最常用的入口二进制文件是/bin/bash,所以我们可以将其替换为#!/proc/self/exe来让runc文件执行自身。

为什么需要先让runc执行自己,再通过/proc/[runc-pid]/exe进行覆盖,而不是直接使用子进程的/proc/[runc-init-pid]/exe进行覆盖呢?这样不是更简单吗?

原因是docker几年前有一个漏洞(CVE-2016-9962),而为了修补这个漏洞,程序在进入docker之前将runc init程序设置为”不可转储”,而在CVE-2019-5736的上下文环境中,’non-dumpable’标志拒绝其他进程解除引用runc init进程的/proc/[pid]/exe,所以我们就无法保存它的文件描述符并修改它了,减轻了通过/proc/[runc-init-pid]/exe覆盖runC二进制文件的问题。这个时候就需要让runc执行自己了,因为runc init会调用execve来执行runc,而execve会丢弃这个标志,所以我们就可以访问新的runc进程/proc/[runc-pid]/exe了。

这样一来,我们就能成功覆盖宿主机上的runc文件,接下来就是再次运行它实现代码执行了。

总结一下,我们需要在docker容器内运行一个脚本,这个脚本修改bash,并在用户从宿主机对容器进行访问的时候(exec),即runc文件运行的时候,读取宿主机上的runc文件并获取它的文件描述符,然后等待runc文件的退出并将其覆盖改写为我们想要的样子。

这样一来就有一个缺陷,我们还需要再在宿主机上用exec执行bash。现在我们需要一个自动化攻击的方案,让用户交互最小化。

为了自动执行恶意脚本,攻击者可以runc文件的动态链接,它所需要的动态链接库可以用ldd命令列出。因为runc在运行的时候动态链接到这些库的原因,所以攻击者可以自行build一个恶意镜像,在build的过程中,用恶意版本替换其中的一个动态链接库,从而让runc在运行的时候同时执行恶意脚本。

使用过docker的人应该知道,docker容器在启动的时候有一个启动命令,有时候我们用shell脚本作为容器的启动命令的时候,如果最后不加上一个/bin/bash之类的命令,就会导致容器无法启动。这是因为容器启动的时候,会将这个启动命令以pid=1运行,而一旦这个pid=1的进程结束了,容器也会跟着结束。所以我们就可以利用这一点,在脚本中调用execve来执行对runc的修改,这样可以就可以强制runc进程退出并覆盖,最后pid=1进程结束->终止容器->再次运行runc->代码执行,这样就可以实现自动化执行两次runc,实现了代码执行。

漏洞修复

容器启动时,二进制文件附加到容器中时会创建临时副本并加以密封,防止损害宿主机。


本人水平有限,如有错误请指出Orz

参考链接:

https://www.twistlock.com/labs-blog/breaking-docker-via-runc-explaining-cve-2019-5736/

http://sec.sangfor.com.cn/events/198.html

https://x3fwy.bitcron.com/post/runc-malicious-container-escape

https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html