Skip to main content

Command Palette

Search for a command to run...

Terrier CTF [Part 1]: Network Reconnaissance to SSTI: The Methodology Beneath the Exploit

When Templates Execute More Than Expected

Updated
5 min read
Terrier CTF [Part 1]: Network Reconnaissance to SSTI: The Methodology Beneath the Exploit

Step 0: Network Address Discovery

The Terrier CTF Boot2Root machine presented an interesting challenge from the start: identifying the machine’s IP address on the local network. While this might seem quite straightforward in theory, the practical reality of VM networking configurations often introduces unexpected complexity which is worth documenting.

Usually, most Boot2Root VMs do us a favor and print their IP address to the console when they boot. Without this convinience, we need a more systematic approach to network discovery. I will be demonstrating the approach I used when using VMWare Workstation Pro on my Ubuntu machine.

First, ensure that the networking mode is set to NAT. This ensures that the machines are on the same virtual network segment and can access each other. Following this, in the Advanced settings menu of the dialog box, record the MAC address.

Then using the arp -a command on the shell, we get the list of various IP addresses associated with the physical addresses

Without console IP disclosure, three discovery vectors exist: ARP cache inspection (fast, requires same subnet), nmap subnet sweep (thorough, time-intensive), or DHCP lease examination (requires hypervisor access). ARP cache provided sufficient granularity, two candidates versus 254; making it the optimal effort-to-information ratio. The ARP command shows the list of IP addresses and their corresponding MAC addresses that your system has recently communicated with on the local network

💡
Documentation note: Screenshots in this series span multiple testing sessions, so you may notice the target IP address changes between sections. This reflects the dynamic nature of DHCP in virtualized environments and doesn't affect the methodology demonstrated.

Step 1: Reconnaissance

Now that we have the IP address, I proceeded with a standard nmap scan to identify open ports. The scan revealed ports 22 (SSH) and 5000 (HTTP) were accessible.

The presence of password based authentication in ssh shows it’s a worthy attack vector, and opening accessing the port 5000 using a web-browser reveals a static web page titled R&D portal.

SSH exploit:

SSH authentication without username enumeration or organizational context is at best, brute force. This approach is statistically futile in CTF environments which are designed around exploiting than guessing. Port 5000 suggests custom application development ( HTTP service on non-standard port ), indicating a higher probability of implementation vulnerabilities than the hardened SSH daemons.

Web page exploit:

The homepage served on port 5000 appeared completely static; buttons were non-functional, no dynamic content was visible, and no obvious navigation paths existed. This warranted directory enumeration using gobuster with the DirBuster wordlist from SecLists, which discovered a /page endpoint containing a text input field with greeting functionality.

The greeting mechanism suggested potential Server-Side Template Injection. Arithmetic evaluation ({{7×7}}→49) provides unambiguous confirmation. If the server returns ‘Hello 49!’, template processing occurred server side. String operations could reflect client-side JavaScript. Mathematical mutation offers binary clarity, either the template executed, or it didn’t.

💡
SSTI ( Server Side Template Injection ) occurs when web applications unsafely embed user input directly into template engines (Jinja2, Twig etc) allowing attackers to inject malicious template directives which execute as server side code. Read more here

As a standard practice, I tried to find out all the available classes using the payload {{''.class.mro[1].subclasses()}}. Effectively, this payload traverses to the base object class of the empty string using the MRO method, which can traverse the parent classes of a given class. The subclasses() method helps us enumerate the currently available modules, which may be used to set up a reverse-shell.

Upon running the payload, a long list of empty strings along with a singular class name was returned It looked something like [, , , , , , , , ,…..,typing.Any, , , , …..] In order to optimize the effort, I tried searching for the _wrap_close module, which helps in gaining access to the OS module. The _wrap_close wraps file like objects, and critically its __init__.__globals__ dictionary typically references to system modules like sys, os which are required for file operations. It is a reliable, and well documented path from the template context to OS-level execution. It is consistently available across Python versions.

The payload used was {% for sc in ''.class.mro[1].subclasses() %}{% if sc.name == '_wrap_close' %}{{ loop.index0 }}{% endif %}{% endfor %}

Running this command is expected to return is the index of the _wrap_close module, and sure enough, the number 140 was returned.

The next step is trying to access the os module from this, so I first tried this payload: {{ ''.class.mro[1].subclasses()[140].init.globals['os'].listdir() }}. This payload effectively tried to access the os module via the __globals__ directory of the class’ __init__ method. os.listdir() is a basic function which lists the files in the current directory.

The direct __globals__['os'] approach returned HTTP 500, indicating either namespace limitations or filtering. Since __builtins__ provides Python's core import machinery and typically exists in all execution contexts, pivoting to dynamic import via __import__ bypasses potential restrictions on pre-imported modules. I tried the payload {{ ‘‘.__class__.__mro__[1].__subclasses__()[140].__init__.__globals__[‘__builtins__’][‘__import__’](‘os’).popen(‘ls -la’).read() }}

The payload finally succeeded, and the output shows the contents of the filesystem of the www-data user, which is a common username used for service accounts used to serve web content over the server. One of the file, F14@_0n3.txt stands out in particular, and may reveal the value of the first flag. The app.py is probably the process which the application connects to, reading this could provide more vulnerabilities or logic flaws.

Screenshot of the directory listing returned by the payload

Slightly modifying the payload to read the F14@_0n3.txt file instead of listing the directory structure reveals the first flag to us.

The first flag secured, but www-data user access is merely a foothold, not privilege. The SSTI vulnerability that gave us file reading capabilities can offer something far more valuable: Arbitrary Command Execution. This is a common way of establishing presence.

💡
Next in series: Terrier CTF [Part 2]: PCAP Dissection: Carving Secrets from Captured Packets
💡
Shout out to my friend, Abhijeet for the awesome cover image he designed!

Indian Army Terrier CTF 2025

Part 1 of 1

Terrier CTF Boot2Root: Systematic documentation of the privilege escalation chain from www-data to root. Every dead end, false lead, and moment of confusion preserved for educational purposes. Because failure teaches more than success.