// HackTheBox

GoodGames

SQLi auth bypass → SSTI RCE → bind-mount container escape to host root

HackTheBoxEasyLinux Jul 21, 2026 5 min read

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 WHERE clause 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 a UNION SELECT grafts 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 is augustus, whose SSH password is not superadministrator. Since we can write his home directory through the mount, plant an SSH key instead of guessing the password — drop our public key into his authorized_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/bash onto the mount and running it on the host fails — the host is a different distro and is missing libtinfo.so.5, so that binary won’t run there. The fix is to use the host’s own bash. As augustus, copy the host’s /bin/bash into his home directory:

    $ cp /bin/bash /home/augustus/hostbash
    

    Then, from container-root through the mount, take ownership and set the SUID bit (the 4 in 4777 means “run as the file’s owner”):

    $ chown root:root /home/augustus/hostbash
    $ chmod 4777 /home/augustus/hostbash
    

    Now hostbash is a root-owned SUID copy of the host’s shell. Back as augustus, run it with -p so it keeps its elevated privileges instead of dropping them:

    $ ./hostbash -p
    

    That yields euid=0 on the host itself — root, and root.txt.

← All writeups