CVE-2024-45310 — runc 符号链接竞争导致宿主机 inode 创建¶
发布于 2026-06-06
摘要
控制共享卷内容的容器侧攻击者,可以通过在共享卷中赢得符号链接替换竞争,诱使 runc 在宿主机文件系统的任意路径上创建空文件或空目录。
| 字段 | 值 |
|---|---|
| Project | runc |
| 受影响组件 | libcontainer/rootfs_linux.go — createMountpoint / 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 中引入(补丁提交 63c2908、8781993、f0b652e),将所有针对 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 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,重启容器即可恢复干净基线:
3. 利用过程¶
利用程序由两个 shell 脚本组成:exploit/run.sh(外层驱动,在宿主机上运行)和 exploit/inner.sh(竞争引擎,注入内层宿主机容器执行)。文件可下载:
竞争原理¶
inner.sh 在内层宿主机容器内并发执行三件事:
- Bundle 构建器: 组装一个最小 OCI bundle(
config.json),其第二个 bind-mount 的目标路径为/share/target/gift,其中/share是攻击者控制的/srv/share-backing的绑定挂载。 - 后台交换器: 运行一个紧密循环,将
/srv/share-backing/target在真实目录与指向/host-target的符号链接之间来回切换。这是竞争诱饵。 - 前台启动器: 反复调用
runc run和runc delete对该 bundle 进行操作。每次调用都会触发 runc 的createMountpoint→securejoin.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 步 — 运行利用程序
| 参数 | 使用值 | 含义 |
|---|---|---|
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:
已验证运行中观测到的输出:
这确认了以下三点:
- 存在性,基线时缺失: 利用运行前
/host-target/gift为No such file or directory。 - 所有者为宿主机 root:
root:root证明是 runc 而非无特权攻击者创建了该 inode;攻击者没有绕过 runc 直接写入/host-target的途径。 - 位于 rootfs 和共享卷之外: inode 编号
33687783与/srv/share-backing/target(inode33687784)不同,且路径位于/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
环境清理¶
创建的 inode 仅存在于内层宿主机容器的文件系统上,执行 docker compose restart 或 down -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 流传。
参考资料¶
- GHSA-jfvp-7x6p-h2pv — GitHub 安全公告:受影响/已修复版本、CVSS、CWE、致谢
- NVD CVE-2024-45310 — CVE 条目,CWE,CVSS
- 补丁提交 63c2908 (main) — 主要差异:
rootfs_linux.go、utils_unix.go、system/linux.go - 补丁提交 8781993 (release-1.1) — release-1.1
createMountpoint整合 - 补丁提交 f0b652e (release-1.1) — release-1.1
MkdirAllInRoot替换 - PR #4359 (main) — 整合意图说明
- oss-security 披露 2024-09-03 — 公开声明;披露时无 PoC