Awesome-POC 云安全专题:Docker cp 容器逃逸漏洞 CVE-2019-14271 原理剖析与实战复现
Awesome-POC 云安全专题:Docker cp 容器逃逸漏洞 CVE-2019-14271 原理剖析与实战复现
本指南以 Awesome-POC 云安全漏洞知识库中的 Docker copy 漏洞导致容器逃逸 CVE-2019-14271 为核心,完整讲解 docker cp 命令背后的 docker-tar 帮助进程、Go cgo 与 libnss_*.so 动态库加载机制如何组合成一条"复制文件即逃逸"的完整攻击链。读者在阅读后,将掌握该漏洞的根因、影响版本、Ubuntu 18.04 漏洞环境搭建方法、端到端逃逸复现步骤,以及基于源码结构推断出的恶意共享库劫持原理与修复建议。
漏洞概述:一次"复制文件"操作引发的容器逃逸
docker cp 命令允许从容器、向容器中、或容器之间复制文件,语法与标准的 Unix cp 命令非常相似。例如要从容器中复制 /var/logs,语法为:
docker cp container_name:/var/logs /some/host/path
看似普通的文件复制操作,却在 Docker 19.03.8 之前被 CVE-2019-14271 赋予了"容器逃逸"的严重性——攻击者只要能让宿主机用户对恶意容器执行 docker cp,就有机会获取宿主机的完全 root 访问权限。
该漏洞的独特之处在于:逃逸的触发方是宿主机上的合法进程,而恶意代码的载体却藏在容器的文件系统里。理解这条链路,需要先弄清 docker cp 的内部实现。
漏洞原理深度解析
docker-tar:chroot 进入容器的"特权搬运工"
在复制过程中,Docker 使用了一个名为 docker-tar 的帮助进程。其工作流程为:
docker-tar通过chroot进入目标容器的根文件系统;- 在容器内将请求的文件或目录打包(archive)成 tar 流;
- 将生成的 tar 数据传递给 Docker daemon;
- 由 daemon 在宿主机的目标目录中完成解包提取。
关键点在于:chroot 只改变了进程对文件系统根的"视角",并没有改变进程本身的权限与身份。docker-tar 仍然是运行在宿主机上、以 root 权限执行的进程。这意味着,任何能在该进程内被加载并执行的代码,都天然拥有宿主机的 root 能力——问题只在于如何把恶意代码"塞进"这个进程。
根因:Go cgo 与 libnss 动态库加载
有漏洞的 Docker 版本是用 Go v1.11 编译的。在该版本中,一些含有嵌入 C 代码(cgo)的包会在运行时动态加载共享库。这些包包括 net 和 os/user,二者都是 docker-tar 所使用的,而且在运行时会加载多个 libnss_*.so 库。
这一机制的背景是 Linux 系统的 NSS(Name Service Switch,名称服务切换) 框架:
- glibc 提供的
getpwnam()、getpwuid()等用户/组解析函数,通过/etc/nsswitch.conf配置文件决定调用哪个后端模块(本地文件、DNS、LDAP 等); - 这些后端模块正是
libnss_files.so.2、libnss_dns.so.2一类的动态共享库; - Go 的
os/user包在启用 cgo 时会调用上述 C 函数,net包同理,从而触发 glibc 通过dlopen动态加载对应 NSS 模块。
漏洞的引爆点就在动态库的加载路径上:一般来说,库是从宿主机的文件系统加载的;但因为 docker-tar 已经 chroot 到了容器,动态链接器解析共享库路径时会以容器根为新的根目录,因此会从容器文件系统中加载这些库。也就是说,docker-tar 会加载并执行来源于容器、或由容器控制的代码。
攻击链:从恶意 .so 到宿主机 root
综合上述机制,完整的攻击链可以归纳为:
- 攻击者构造一个恶意
libnss_*.so(典型目标是libnss_files.so.2),并将其放置到容器的/lib/x86_64-linux-gnu/(或对应架构的库路径)下,覆盖原库; - 宿主机用户(如运维人员)对该容器执行
docker cp; - Docker daemon 启动
docker-tar,chroot进入容器文件系统; docker-tar运行时,Go 的os/user/net包触发 glibc 加载 NSS 模块;- 动态链接器从容器内加载被替换的恶意
libnss_files.so.2,其构造函数中的恶意代码以宿主机 root 身份执行; - 恶意代码完成逃逸,攻击者获得宿主机上的 root 代码执行权限。
从源码结构看,这类利用的核心在于"受害者进程的权限边界与文件系统视角不一致"——进程拥有宿主机的全部权限,却被诱骗加载了攻击者控制下的代码,本质上是一种针对动态链接信任边界的劫持。
漏洞影响范围与攻击场景
影响版本范围如下:
v18.09.9 <= Docker < v19.03.8
即 Docker 18.09.x 系列受影响版本(自 18.09.9 起)直至 19.03.7 均存在该漏洞,19.03.8 及之后的版本已修复。
可能的攻击场景为 Docker 用户从另一个 Docker 处复制文件:
- 场景一:容器运行含有恶意
libnss_*.so库的镜像(如攻击者构造的特制恶意镜像); - 场景二:容器中含有被攻击者替换的
libnss_*.so库(攻击者已获得容器内文件系统写入能力,如通过容器内的另一个漏洞先立足)。
在这两种情况下,只要宿主机用户对该容器执行 docker cp(从容器向宿主机复制文件),攻击者都可以获取主机上的 root 代码执行权限。值得注意的是,该漏洞的触发条件相对隐蔽:受害者执行的是一条看似常规、低风险的管理命令。
环境搭建:Ubuntu 18.04 + Docker 19.03.0
复现该漏洞需要一个位于受影响区间内的 Docker 版本。以下以 Ubuntu 18.04 为例,使用 install_docker_19.03.0.sh 脚本安装 Docker 19.03.0:
#!/bin/bash
set -e
echo "[*] Removing old Docker versions (if any)..."
sudo apt remove -y docker docker-engine docker.io containerd runc || true
echo "[*] Unholding previously held Docker packages (if any)..."
sudo apt-mark unhold docker-ce docker-ce-cli containerd.io || true
echo "[*] Removing incorrect Docker sources..."
sudo rm -f /etc/apt/sources.list.d/docker.list || true
sudo sed -i '/download.docker.com/d' /etc/apt/sources.list
echo "[*] Adding Tsinghua University Docker mirror GPG key..."
wget -qO - https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
echo "[*] Adding Tsinghua University Docker mirror repository..."
echo "deb [arch=amd64] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu bionic stable" \
| sudo tee /etc/apt/sources.list.d/docker.list
echo "[*] Updating package index..."
sudo apt update
echo "[*] Searching for Docker 19.03.0..."
VERSION_STRING=$(apt-cache madison docker-ce | grep 19.03.0 | head -n1 | awk '{print $3}')
if [ -z "$VERSION_STRING" ]; then
echo "[*] Docker 19.03.0 not found"
exit 1
fi
echo "[*] Found version: $VERSION_STRING"
echo "[*] Installing Docker version $VERSION_STRING ..."
sudo apt install -y docker-ce=$VERSION_STRING docker-ce-cli=$VERSION_STRING containerd.io
echo "[*] Locking version to prevent automatic updates..."
sudo apt-mark hold docker-ce docker-ce-cli containerd.io
echo "[*] Installation complete, current version:"
docker --version
该脚本的关键步骤说明如下:
| 步骤 | 作用 |
|---|---|
apt remove 旧版本 |
清理可能存在的 docker、docker-engine、docker.io、containerd、runc,避免版本冲突 |
apt-mark unhold |
解除之前可能设置的 apt 版本锁定 |
| 清理 Docker 源 | 删除错误的 /etc/apt/sources.list.d/docker.list 及旧源配置 |
| 添加清华镜像源 | 使用清华大学 Docker CE 镜像加速国内下载,bionic 对应 Ubuntu 18.04 |
apt-cache madison 查询版本 |
精确匹配 19.03.0 的版本字符串,避免安装到其他版本 |
apt install -y 指定版本 |
同时锁定 docker-ce、docker-ce-cli、containerd.io 三个组件版本 |
apt-mark hold |
锁定版本,防止后续 apt upgrade 自动升级修复漏洞 |
安装完成后,可通过 docker --version 确认版本号为 19.03.0:
关于漏洞环境版本选择的补充:Awesome-POC 的 Kubernetes + Ubuntu 18.04 漏洞环境搭建 文档在"容器逃逸"类别中收录了 CVE-2019-14271,并说明其漏洞环境基于 Docker 18.09.3/19.03.6 构建,同样落在受影响区间内,可作为搭建整套容器/集群漏洞靶场的参考。该文档也明确提示:此类安装脚本仅适用于漏洞复现环境,请勿用于生产环境。
漏洞复现:从恶意库替换到 root 逃逸
步骤 1:创建目标容器
新建一个名为 14271 的容器,镜像使用 ubuntu,运行 bash 保持存活:
sudo docker run -itd --name=14271 ubuntu bash
步骤 2:上传利用文件
将漏洞利用文件目录拷贝至容器内。利用文件来自 Metarget 项目的 docker-cve-2019-14271/exp 目录(该目录结构与 breakout、libnss_files.so.2、original_libnss_files.so.2 三个文件相关,具体解读见后文"利用文件结构与逃逸机制解读"):
sudo docker cp ./metarget/writeups_cnv/docker-cve-2019-14271/exp/ 14271:/
步骤 3:替换容器内原有的 NSS 库
进入容器,删除容器内原来的 .so 文件,替换为修改后的恶意 .so 文件:
sudo docker exec -it 14271 bash
root@7271b49ce3d1:/# ls /exp
breakout libnss_files.so.2 original_libnss_files.so.2
root@7271b49ce3d1:/# cp /exp/* /
root@7271b49ce3d1:/# chmod 777 /breakout
root@7271b49ce3d1:/# touch /logs
root@7271b49ce3d1:/# rm /lib/x86_64-linux-gnu/libnss_files.so.2
root@7271b49ce3d1:/# mv /libnss_files.so.2 /lib/x86_64-linux-gnu/
root@7271b49ce3d1:/# exit
exit
各命令的意图如下:
ls /exp:查看利用文件,共包含breakout(逃逸辅助程序)、libnss_files.so.2(恶意 NSS 库)、original_libnss_files.so.2(原始库备份)三个文件;cp /exp/* /:将三个文件复制到容器根目录/,便于后续操作;chmod 777 /breakout:为breakout赋予可执行权限;touch /logs:在容器根目录创建/logs文件,作为后续docker cp的复制目标,确保触发时docker-tar被拉起并执行归档流程;rm原始库 +mv恶意库:删除容器内原有的/lib/x86_64-linux-gnu/libnss_files.so.2,将恶意libnss_files.so.2移动到该标准库路径,确保动态链接器在容器根视角下加载到的是恶意版本。
步骤 4:在宿主机触发 docker cp 完成逃逸
替换完成后退出容器,回到宿主机执行 docker cp 命令,将容器内的 /logs 复制到当前目录:
sudo docker cp 14271:/logs ./
当这条命令执行时,Docker daemon 启动 docker-tar,后者 chroot 进容器并加载被替换的恶意 libnss_files.so.2,恶意代码随即以宿主机 root 权限执行。
步骤 5:验证逃逸结果
再次进入容器,检查根目录下是否存在 host_fs 目录:
sudo docker exec -it 14271 bash
root@7271b49ce3d1:/# ls -al / | grep host_fs
drwxr-xr-x 24 root root 4096 May 26 05:46 host_fs
host_fs 目录的出现意味着宿主机文件系统已经被 bind mount 到容器内部。此时攻击者已在容器内获得对整个宿主机文件系统的读写视角,可以进一步读取 /etc/shadow、植入 SSH 公钥、篡改系统文件或写入持久化后门,容器逃逸宣告完成。
利用文件结构与逃逸机制解读
从利用文件的三件套结构(breakout、libnss_files.so.2、original_libnss_files.so.2)以及复现流程可以推断其逃逸手法:
libnss_files.so.2(恶意库):在原始 glibclibnss_files.so.2基础上植入恶意逻辑,利用 ELF 共享库的构造函数(constructor)机制,在dlopen加载时立即执行。此时进程处于chroot后的容器视角,但拥有宿主机 root 权限与完整的挂载能力;breakout(逃逸辅助程序):恶意库加载后执行的落脚点。典型手法是借助进程当前工作目录(cwd)可能仍指向chroot根之外的特性,通过反复chdir("..")跳出chroot回到宿主机真实根目录,随后执行breakout;breakout通常负责在容器内创建/host_fs目录,并将宿主机根目录/以 bind mount 方式挂载进去,从而把宿主机文件系统"呈现"给容器内视角;original_libnss_files.so.2(原始库备份):保存原始库文件,供恢复或对比分析使用,也说明利用制作过程是"基于原始库改造成恶意库"。
需要说明的是,上述细节是基于文件命名与复现流程的合理推断,仓库内并未包含该 exp 的源码;实际利用的精确实现以 Metarget 项目提供的文件为准。但无论实现细节如何,其本质始终一致:劫持宿主机进程(docker-tar)在容器文件系统中动态加载共享库的行为。
漏洞修复与防御建议
升级修复
最直接的修复方式是将 Docker 升级至修复该漏洞的版本(19.03.8 及之后,或包含修复的更高版本)。生产环境应通过官方渠道跟踪 Docker Engine 的版本更新,及时应用安全补丁。对于复现环境,同样建议在完成验证后立即升级,避免将存在高危逃逸漏洞的 Docker 暴露在可访问的网络中。
缓解与防御措施
从漏洞成因出发,可以结合实际情况考虑以下缓解措施:
- 镜像信任边界:仅从可信来源拉取镜像,并对镜像内容进行审计,避免运行内含可疑动态库的恶意镜像(对应"场景一");
- 容器内文件完整性:对容器文件系统建立基线并监控关键库文件(如
/lib/x86_64-linux-gnu/libnss_*.so)的变更,及时发现被替换的可疑共享库(对应"场景二"); - 最小权限原则:限制普通用户对
docker组和 Docker API 的访问,避免不可信用户触发针对敏感容器的docker cp操作; - 纵深防御:为 Docker daemon 与容器运行时配置 seccomp、AppArmor 等安全加固策略,降低共享库劫持类攻击成功后的影响面。
延伸阅读:Awesome-POC 中的容器逃逸系列
CVE-2019-14271 只是容器逃逸攻击面的一环。Awesome-POC 云安全漏洞知识库中收录了完整的容器/集群逃逸专题,可在 README.md 的"云安全漏洞"分类下交叉查阅:
- Docker runC 漏洞导致容器逃逸 CVE-2019-5736:通过恶意容器覆盖 runC 二进制实现宿主机代码执行;
- Containerd 漏洞导致容器逃逸 CVE-2020-15257:利用 containerd-shim API 暴露的 Unix socket 逃逸;
- 挂载 docker.sock 导致容器逃逸、挂载宿主机 procfs 系统导致容器逃逸:利用错误挂载获得宿主机控制权;
- Linux 内核 cgroup v1 逻辑错误导致容器逃逸 CVE-2022-0492:内核层面的逃逸利用。
这些文档与本文共同构成了从"应用层逃逸"到"内核层逃逸"的完整知识图谱,便于安全研究人员系统性地理解容器隔离边界的各类突破方式。
免责声明:本文内容仅用于安全研究与教学目的。对漏洞环境的搭建与复现,请在隔离的、获得授权的实验环境中进行,严禁用于任何未授权系统或生产环境。