Terrier CTF [Part 1]: Network Reconnaissance to SSTI: The Methodology Beneath the Exploit
When Templates Execute More Than Expected
![Terrier CTF [Part 1]: Network Reconnaissance to SSTI: The Methodology Beneath the Exploit](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1760333413720%2Fa5955db5-f349-4ffb-b1c1-25a052b8c4c4.jpeg&w=3840&q=75)
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
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.

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.

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.



