一、概述
以下漏洞研究的灵感来源于35C3 CTF的namespaces任务,由_tsuro创建。在进行这一挑战的过程中,我们发现,从安全角度来看,要创建基于命名空间的沙箱并通过外部进程加入是一项非常具有挑战性的任务。我们在CTF比赛结束后,发现Docker具有“docker exec”功能(实际上是由opencontainers的runc实现的)具有类似的模型,于是我们决定挑战这种漏洞实现。
二、目标及结果
我们的目标是在默认配置或加固后配置下的Docker容器内部攻陷宿主机环境(例如:获得有限的功能和系统调用可用性)。我们考虑了以下两个攻击维度:
1、恶意Docker镜像;
2、容器内的恶意进程(例如:攻陷以root身份运行的Docker化服务)。
最终的结果,我们已经在宿主机上实现了完整意义的代码执行,具有所有功能(获得管理员“root”访问权限),该过程可以由以下任一方式触发:
1、在被攻陷的Docker容器上,从宿主机运行“docker exec”;
2、启动恶意Docker镜像。
该漏洞被分配CVE编号CVE-2019-5736,并在这里正式公布:https://seclists.org/oss-sec/2019/q1/119
三、默认Docker安全设置
尽管Docker并没有作为沙盒软件发行,但它的默认设置会保护宿主机资源不被容器内的进程访问。尽管Docker容器中的初始进程是以root身份运行的,但实际上它具有非常有限的权限,这是通过几种不同的机制来实现的,这篇文章对其进行了详细描述。
1、Linux功能
默认情况下,Docker容器具有非常有限的功能集,这使得容器root用户实际上是一个非特权用户。
2、seccomp
这一机制能够阻止容器的进程执行系统调用的子集或者过滤其参数,从而限制其对宿主机环境的影响。
这一机制允许限制容器化进程对宿主机文件系统的访问,并限制跨主机、跨容器边界进程之间的可见性。
4、cgroups
控制组(cgroups)机制允许限制和管理一组进程的各种类型的资源(例如:RAM、CPU等)。
上面的这些机制都可以被禁用(例如:通过使用—privileged命令行选项),或者明确指定任何一组系统调用/功能/共享的命名空间。如果禁用了这些加固机制,就可以轻松实现容器逃逸。但我们此次尝试将在默认安全配置的Docker容器中进行。
四、失败的漏洞利用方案
在最终找到这一漏洞之前,我们尝试过许多其他想法,其中大部分思路都是通过有限的功能或者seccomp过滤器来实现的。
由于这一研究过程是在35C3 CTF之后的后续工作,因此我们首先要调查在现有命名空间中启动新进程时会发生什么(也就是docker exec)。我们的目标是,检查是否可以通过新加入的进程获取它们,以访问某些宿主机资源。想象以下场景,其中的某个进程:
1、加入用户和PID命名空间;
2、forks(实际加入PID命名空间);
3、加入其余的命名空间(mount、net等)。
如果我们可以在看到它时(也就是该进程加入PID命名空间时),立即对该进程进行ptrace,就可以阻止它加入其它的命名空间,但反过来又可以启用。
通过容器初始化进程执行为共享的用户命名空间,可以绕过不必须的ptrace功能(会产生新用户命名空间中的完整功能集)。然后,“docker exec”将会加入到新的命名空间(通过/proc/pid/ns获得),我们可以在其中进行ptrace(但仍然具有seccomp的限制)。事实证明,runc在完成之后,加入了所有必须的命名空间,并在完成后只进行fork,这样就成功阻止了这一攻击向量。此外,默认的Docker配置还会禁用容器内所有与命名空间相关的系统调用(例如:setns、unshare等)。
接下来,我们专注于proc文件系统(proc(5),更多信息请参考:http://man7.org/linux/man-pages/man5/proc.5.html),因为它非常特殊,并且通常可以跨越命名空间的边界。其中,有如下几个有趣的条目:
1、/proc/pid/mem:由于目标进程需要在与恶意进程相同的PID命名空间中,因此这不能给我们带来太多帮助。同样,ptrace(2)也是如此。
2、/proc/pid/cwd和/proc/pid/root:在进程完全加入容器之前(加入命名空间之后,更新其根目录chroot和cwd cwdir之前),这些条目都指向宿主机文件系统,可能允许我们对其进行访问。但由于runc进程不可转储(http://man7.org/linux/man-pages/man2/ptrace.2.html),因此我们无法利用。
3、/proc/pid/exe:本身没有任何用处(与第2条相同的原因),但我们已经找到了解决方法,并在最终的漏洞利用方案中利用了它们(详见下文)。
4、/proc/pid/fd/:某些文件描述符可能从父级命名空间(特别是mount命名空间)中泄露,或者我们可能会在runc中干扰父子间(实际上是孙子间)的通信。我们无法利用这里,因为同步过程已经使用过了本地套接字,不能再重复使用。
5、/proc/pid/map_files/:这是一个非常有趣的向量。在runc执行目标二进制文件之前(在进程对我们可见,即进程加入PID命名空间之后),所有条目都引用来自宿主机文件系统的二进制文件。不幸的是,我们发现,如果没有SYS_ADMIN功能(源代码:https://elixir.bootlin.com/linux/v4.20.7/source/fs/proc/base.c#L2037),我们就无法利用这些链接,即使在同一个进程中也是如此。
附注1:在执行以下命令时
/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 exec”使用来自宿主机的动态加载器来执行容器内的二进制文件/evil_binary。具体而言,是使用文本文件第一行的内容#!/proc/self/map_files/address-in-memory-of-ld.so替换原始目标到exec(例如:/bin/bash)。
随后,/evil_binary就可以覆盖/proc/self/exe,从而覆盖宿主机的ld.so。由于上述所提到的SYS_ADMIN功能要求,因此这一方法不能成功。
附注2:在尝试上面的内容时,我们在内核中发现了一个死锁。当一个常规进程试图执行“/proc/self/map_files/any-existing-entry”时,将会产生死锁。从任何其他进程打开“/proc/that-process-pid/maps”时也会发生挂起,可能也产生了一些锁定。
五、成功的漏洞利用方案
最后,我们成功的漏洞利用方案涉及到一种非常类似于上述思路的方法,利用到/proc/self/map_files。我们执行/proc/self/exe,这是主机的docker-runc二进制文件,同时可以向其中注入一些代码。具体而言,我们首先更改某些共享库(例如:libc.so),从而执行我们的代码(例如:在libc_start_main或全局构造函数中)。这使我们能够覆盖/proc/self/exe二进制文件,该文件是来自宿主机的docker-runc二进制文件,反过来也会在下次执行docker-runc时为我们提供对宿主机的完全访问权限。
详细攻击说明如下。
首先,制作恶意镜像,或攻陷正在运行的容器:
1、将入口点二进制文件(或任何可能被用户作为入口点或作为docker exec一部分被运行时覆盖的二进制文件)称为/proc/self/exe的符号链接;
2、将docker-runc使用的任何动态库替换为具有附加全局构造函数的自定义.so。该函数将打开/proc/self/exe(指向主机docker-run)进行读取(无法打开它进行写入操作,因为二进制文件正在执行中,请参考open(2)中的ETXTBSY)。然后,这个函数执行另一个打开的二进制文件/proc/self/fd/3(在execve之前打开的docker-runc的文件描述符),现在就要进行写入了。这一过程能成功执行,因为docker-runc在此时不会再被执行。然后,代码可以使用任意内容来覆盖宿主机的docker-runc。我们选择了一个假的docker-runc,以及额外一个运行任意代码的全局构造函数。
因此,当主机用户在受感染的容器上运行受感染的镜像或“docker exec”时:
1、已经符号链接到/proc/self/exe(在宿主机文件系统上指向docker-runc)的入口点/exec二进制文件开始在容器内执行。这一过程会导致进程可以转储,因为execve设置了可以转储的标志。需要明确的是:这会导致原始docker-runc进程重新执行在容器内运行的新docker-runc(但使用的是宿主机二进制文件)。
2、当docker-runc第二次开始执行时,它将从容器(而不是宿主机)下载.so文件。而我们已经能够控制这些动态库的内容。
3、执行恶意全局构造函数。它将打开/proc/self/exe进行读取(例如文件描述符3),以及execve()一些攻击者控制的二进制文件(例如:/evil)。
4、/evil将覆盖宿主机文件系统上的docker-runc(通过重新打开fd3,这次具有写访问权限)和具有后门或恶意的docker-runc(例如:使用额外的全局构造函数)。
5、当任何容器启动,或其他exec完成时,攻击者构造的docker-runc将以root身份执行,并且具有宿主机文件系统的全部功能(该二进制文件负责删除权限并进入命名空间,因此其最初具有完全权限)。
请注意,这一攻击过程仅滥用了runc(opencontainers)的行为,因此它也同样适用于Kubernetes,无论是否使用docker或cri-o。
该攻击将对AWS和GCP云服务产生严重影响,有关更多信息可以在安全公告中查看。
六、负责任的漏洞披露过程
我们在发现这一漏洞的当天,向[email protected]报告了漏洞,其中包含详细的攻击描述和概念验证。转天,Docker安全团队将我们的电子邮件转发至[email protected]。在此过程中,我们积极参与有关漏洞修复的讨论,与Docker和OpenContainers安全团队进行了密切的沟通。
七、失败的runc修复方案
方案1:打开目标二进制文件,将fstat(2)中inode信息与/proc/self/exe进行比较,如果匹配则退出,否则执行目标二进制文件fd。
这将会检测目标二进制文件是否是/proc/self/exe的符号链接。为什么是execveat呢?因为我们想要避免竞态条件(Race Condition)的出现,如果在exec之间进行比较,其他一些进程会将目标二进制文件替换为/proc/self/exe链接。
失败原因:当攻击者没有使用符号链接时,可以绕过这一过程。攻击者可以使用带有指向“/proc/self/exe”的动态加载程序的二进制文件,将“#!/proc/self/exe”作为第一行,或者只是作为一个elf文件。
方案2:使用静态二进制文件,启动容器内的进程。
这样做的想法是通过容器内的恶意.so文件来避免代码执行的可能性(静态二进制文件就意味着没有加载.so文件)。
失败原因:该漏洞利用实际上不需要替换.so文件。在重新执行/proc/self/exe(docker-runc)之后,另一个进程可以打开/proc/<pid-of-docker-runc>/exe,这是可能的,因为execve上设置了“dumpable”(可转储)标志。这一过程有些难以实际应用,因为它需要在重新执行完成和runc进程退出之间进行竞态(由于没有给出参数)。实际上,竞态的窗口期非常大,我们能够为这种情况开发出100%成功的漏洞利用方案。然而,这种方法确实可以消除其中的一个攻击向量,也就是运行恶意镜像。
八、最终runc修复方案
最后,应用以下修复程序来缓解漏洞:
1、创建一个memfd(仅存在于内存中的特殊文件)。
2、将原始runc二进制文件复制到这一fd中。
3、在进入命名空间之前,从这个fd重新执行runc。
这一修复程序能够保证,如果攻击者覆盖了/proc/self/exe指向的二进制文件,将不会对宿主机造成任何损害,因为这是宿主机二进制文件的副本,完全存储在内存中(tmpfs)。
九、缓解方案
如果当前使用未修复的runc,可以采用如下的缓解方案:
1、使用启用SELinux的Docker容器(–selinux-enabled),从而防止容器内的进程覆盖宿主机docker-runc二进制文件。
2、在宿主机上使用只读文件系统,至少使用只读方式来存储docker-runc二进制文件。
3、在容器内,使用低权限用户,或者使用映射到该用户的uid 0的新用户命名空间。并且,该用户不应对宿主机上runc二进制文件具有写访问权限。
十、时间线
2019年1月1日 发现漏洞并编写PoC;
2019年1月1日 向[email protected]报告漏洞;
2019年1月2日 由Docker安全团队将漏洞报告转交给[email protected];
2019年1月5日 进行漏洞利用思路的调整;
2019年2月11日 可以公布CVE-2019-5736详情;
2019年2月13日 发布本文章。
十一、官方修复方案(译者加)
截至本文翻译完成时,runc、Docker、Kubernetes、亚马逊云AWS均已发布官方修复方案,具体如下。
1、runc官方:https://github.com/opencontainers/runc/commit/0a8e4117e7f715d5fbeef398405813ce8e88558b
2、Docker :已在18.09.2 版本中修复这一漏洞。
https://github.com/docker/docker-ce/releases/tag/v18.09.2
3、Kubernetes:修复方案请参考 https://kubernetes.io/blog/2019/02/11/runc-and-cve-2019-5736 。
4、AWS平台:修复方案请参考https://aws.amazon.com/cn/security/security-bulletins/AWS-2019-002/ 。