// HackTheBox
GoodGames
SQLi auth bypass → SSTI RCE → bind-mount container escape to host root
Recon
Only one port is open:
| Port | Service |
|---|---|
| 80 | HTTP (Werkzeug/Flask) — GoodGames |
The Werkzeug server banner identifies a Python Flask app. A hand-rolled Flask
site (as opposed to an off-the-shelf CMS) means the interesting bugs are likely to
be in the app’s own code — its login form and its templating — rather than in a
known CVE. So the login form is the first thing to probe.
Foothold
1. SQL injection in the login form
The main site’s /login takes an email address, and that field is injectable. Two
things fall out of it:
-
Auth bypass. A classic tautology in the email field makes the
WHEREclause always true, logging us in without a valid password:' OR 1=1-- - -
UNION dump. The query returns four columns and the fourth is reflected back into the page as
Welcome <x>, so aUNION SELECTgrafts our own data onto the result and prints column 4 on screen:' UNION SELECT 1,2,3,4-- -
Swapping real column expressions into position 4 lets us read the database.
Dumping the user table returns the administrator account
admin@goodgames.htb and its password hash 2b22337f218b2d82dfc3b6f77e7cb8ec.
That is a bare 32-hex-character MD5 — unsalted and fast — so it cracks against
rockyou.txt almost instantly:
$ echo 2b22337f218b2d82dfc3b6f77e7cb8ec > hash.txt
$ john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
The plaintext is superadministrator.
2. The admin vhost and its login gotcha
Those credentials don’t unlock anything new on the main site — they belong to a
second app on the vhost internal-administration.goodgames.htb (add it to
/etc/hosts). It’s a separate Flask application using the Volt admin template.
Its login has a trap. The submit control is a named button:
<button type="submit" name="login">
and the route only runs its authentication branch when that login field is
present in the POST body. Reproducing the login outside a browser and omitting
login makes the request always 302 to /index without ever setting the
_user_id session — which looks exactly like a broken or rejected login. Including
login= in the body alongside admin:superadministrator makes the auth branch fire
and logs us in.
3. Server-side template injection → RCE
Inside the admin panel, /settings takes a name value and reflects it back into
the rendered page. Because the app drops that value straight into a Jinja2
template (Flask’s engine), it’s vulnerable to SSTI — server-side template
injection, where input is evaluated as template code rather than printed as text. A
quick arithmetic probe confirms it: submitting {{1337*1337}} renders as
1787569, so the server is evaluating our braces.
From arithmetic to command execution, walk Jinja2’s object graph out to Python’s
os module and call popen:
{{ cycler.__init__.__globals__.os.popen('CMD').read() }}
Replacing CMD runs shell commands and returns their output. This lands us as
root, but inside a Docker container (an isolated host with ID 3a453ab...).
The container has /home/augustus bind-mounted in from the real host’s /dev/sda1
disk — a directory shared straight through from the host — so the user flag can be
read directly off that mount.
Privilege Escalation — bind-mount container-root → host-root
Being root inside the container isn’t root on the host, but the bind mount is the
bridge: files written under /home/augustus from the container land on the host,
owned by root because that’s who we are in the container. The problem is turning
that file-write into host code execution.
-
Reaching the host account. The host’s SSH (port 22) isn’t exposed to the VPN, but the host is reachable from the container over the Docker bridge gateway
172.19.0.1. The target account there isaugustus, whose SSH password is notsuperadministrator. Since we can write his home directory through the mount, plant an SSH key instead of guessing the password — drop our public key into hisauthorized_keys, then log in:$ cat attacker.pub >> /home/augustus/.ssh/authorized_keys $ ssh -i attacker augustus@172.19.0.1 -
From augustus to host root. The clean trick would be to drop a root-owned SUID shell on the mount, but copying the container’s own
/bin/bashonto the mount and running it on the host fails — the host is a different distro and is missinglibtinfo.so.5, so that binary won’t run there. The fix is to use the host’s own bash. Asaugustus, copy the host’s/bin/bashinto his home directory:$ cp /bin/bash /home/augustus/hostbashThen, from container-root through the mount, take ownership and set the SUID bit (the
4in4777means “run as the file’s owner”):$ chown root:root /home/augustus/hostbash $ chmod 4777 /home/augustus/hostbashNow
hostbashis a root-owned SUID copy of the host’s shell. Back asaugustus, run it with-pso it keeps its elevated privileges instead of dropping them:$ ./hostbash -pThat yields
euid=0on the host itself — root, and root.txt.