FlawOpen

flawopen.com/Teardowns/cve-2024-21626-runc-container-breakout-leaky-vessels

● CVE-2024-21626 · CVSS 8.6[1], [5] · 高危
安全研究 · FlawOpen

CVE-2024-21626:runc 如何修复 Leaky Vessels 容器逃逸

CVE-2024-21626 将 runc 内部文件描述符泄漏与 chdir 后缺少边界检查结合起来。根据启动路径,容器可能以位于主机命名空间的工作目录启动,或通过相关 exec 路径访问主机文件。[1], [2], [3]

💡 通俗易懂的原理解析 (ELI5)

runc 为容器准备隔离空间。内部句柄并未直接交给容器进程,但漏洞可能让所选工作目录解析到空间边界之外;修复会检查最终目录,并在启动容器程序前关闭内部句柄。这是运行时目录边界检查问题,并非普通的容器文件继承。[1], [2]

核心概念与专有名词

runc
用于创建和启动容器的 OCI 运行时。[1]
内部文件描述符
操作系统用于引用已打开文件或目录的句柄。如果子进程启动时描述符仍然打开,它可能跨进程边界保留访问能力。[1], [3]
cwd
进程的工作目录。如果目录位于挂载命名空间根目录之外,Linux 的 Getwd 可能返回 ENOENT。[1], [2]
容器逃逸
容器进程访问隔离边界之外主机资源的路径。[1]

框架映射

  • 本指南所述弱点的 CWE 参考: CWE-403

根本原因剖析 (Root Cause)

在 runc 1.1.12 之前,内部文件描述符可能泄漏到 runc init。它们属于 runc init 内部,并非直接继承到容器进程。在攻击 1 和 2 中,配置或指定的工作目录可能通过主机挂载命名空间的句柄解析;runc 在 chdir 后没有验证最终 cwd 是否仍位于容器根目录内。其他变体利用 execve 路径处理覆盖主机二进制文件。1.1.12 版本加入四层措施:利用 Linux Getwd/ENOENT 行为验证 cwd,在 execve 前立即关闭内部描述符,修复已知泄漏,并在启动 runc init 前将非 stdio 描述符设为 close-on-exec。[1], [2], [3], [4]

CVE-2024-21626:攻击变体与 runc 1.1.12 的对应防护

公告中的变体必要条件与边界对应修复或临时措施
攻击 1:通过 runc run 启动恶意镜像需要诱使特权用户启动不可信镜像。镜像配置的工作目录可能解析到容器挂载命名空间之外。chdir 后的 unix.Getwd() 检查会在 cwd 位于命名空间根目录之外时失败。临时措施是对不可信镜像只允许 cwd 为 /。
攻击 2:容器内进程配合 runc exec容器内进程需要预判管理员通过 runc exec --cwd 选择的非 / 工作目录,并在执行前准备相关路径。相同的 chdir 后 cwd 检查会阻止容器外执行。修复前将 --cwd 限制为 /。
攻击 3a:启动镜像时覆盖主机二进制文件这是攻击 1 的变体,仍要求特权用户启动恶意镜像;该变体跨越 execve 边界并触及主机二进制文件。在 execve 前立即关闭 runc 内部描述符,修复已知泄漏,并设置 O_CLOEXEC。升级到 runc 1.1.12 或供应商已修复版本。
攻击 3b:通过 runc exec 覆盖主机二进制文件这是攻击 2 的变体,起点是攻击者能够影响的运行中容器里的恶意进程;但仍需要有特权的管理员对该容器执行一次常规 runc exec 操作。公告指出,这条路径所需的用户交互少于镜像路径。exec 前关闭描述符和 close-on-exec 防护对应这一独立路径。维护者没有给出实用的临时绕过措施;请升级。

攻击执行流程分解

步骤 1

内部句柄泄漏

初始化期间,引用主机挂载命名空间的 runc 内部描述符可能仍对 runc init 可用。[1], [3]

步骤 2

可达的工作目录路径

恶意镜像或满足特定条件的 runc exec 流程可能使请求的 cwd 解析到容器根目录之外。[1], [2]

步骤 3

边界检查缺失

旧代码在 chdir 后没有验证最终 cwd;相关变体利用 execve 路径处理覆盖主机二进制文件。[2], [3]

步骤 4

影响与条件

公告记录了四种变体导致的主机文件系统访问和主机控制。镜像攻击需要特权用户运行不可信镜像。攻击 3b 仍要求管理员对攻击者能够影响的容器执行一次常规 runc exec 操作。[1]

源代码对比:漏洞与安全实现

存在漏洞的实现
// 修复前行为的简化表示;并非 runc 原始源码
if err := os.Chdir(config.Cwd); err != nil {
    return err
}
// 缺少检查:最终 cwd 是否位于命名空间根目录内
加固后的安全修复
// runc 1.1.12 上游边界检查(节选)
if wd, err := unix.Getwd(); errors.Is(err, unix.ENOENT) {
    return errors.New("cwd 位于容器挂载命名空间根目录之外")
} else if err != nil {
    return fmt.Errorf("无法验证 cwd 安全性:%w", err)
} else if !filepath.IsAbs(wd) {
    return fmt.Errorf("cwd 不是绝对路径:%q", wd)
}

工程与系统安全加固清单

参考来源