githubEdit

SSTI

PayloadsAllTheThings SSTIarrow-up-right | SSTI Payloadsarrow-up-right


Confirm SSTI

Break Template Syntax

${{<%[%'"}}%\.

If this causes a server error, SSTI is likely.

Basic Math Test

{{7*7}}
${7*7}
<%= 7*7 %>
${{7*7}}
#{7*7}
*{7*7}

If 49 appears, template is executing code.


Identify Template Engine

Payload
Result
Engine

${7*7}

49

Freemarker, Thymeleaf, etc.

{{7*7}}

49

Jinja2, Twig, etc.

{{7*'7'}}

7777777

Jinja2

{{7*'7'}}

49

Twig

<%= 7*7 %>

49

ERB (Ruby)

#{7*7}

49

Pebble, Thymeleaf

Formal Template Engine Identification (ordered payloads)

Use these payloads in order to narrow down the engine:

Payload
If Rendered
Engine

D{{="O"}}T

DOT

DotJS

P#{XXXXXXX}ug

Pug

PugJS

Thym[[${session}]]eleaf

Thymeleaf

Thymeleaf

Djan{{ Jinja2.Django }}go

Django → Django; Jinja2 → Jinja2

Django or Jinja2

JavaScript (Node.js) Template Engines

When the backend is Node.js/Express (check X-Powered-By: Express), use this table to identify the template engine by its tag syntax:

Template Engine
Payload Format

DotJS

{{= }}

DustJS

{ }

EJS

<% %>

HandlebarsJS

{{ }}

HoganJS

{{ }}

Lodash

{{= }}

MustacheJS

{{ }}

NunjucksJS

{{ }}

PugJS

#{ }

TwigJS

{{ }}

UnderscoreJS

<% %>

VelocityJS

#=set($X="")$X

VueJS

{{ }}

Reference: Template Engines Injection 101arrow-up-right


Jinja2 (Python/Flask)

Information Disclosure

Local File Read

RCE

Alternative Paths to RCE


Twig (PHP/Symfony)

Information Disclosure

Local File Read (Symfony only)

RCE


Nunjucks (Node.js/Express)

Nunjucks HTML-encodes all template variables by default (', ", &, <, >). This breaks payloads containing those characters. Trivial payloads like {{7*7}} still work because they don't use special HTML characters.

Reference: Nunjucks — Exploiting Second-Order SSTIarrow-up-right

Confirm SSTI

If the response reflects 49, the engine is evaluating templates.

RCE via range.constructor

This bypasses HTML encoding issues because the payload uses escaped quotes inside the outer JSON string:

Multi-step shell (avoids special characters in the payload by downloading a script):

Environment Leak


Universal Node.js SSTI Payloads

These work across many Node.js template engines (DotJS, EJS, PugJS, UnderscoreJS, Eta, Nunjucks). Wrap in the appropriate tag for the engine.

Rendered RCE

Error-Based RCE

Boolean-Based RCE

Time-Based RCE


SSTImap (Automated Tool)


Python eval() in f-string / format string

When user input is interpolated into a string that is then passed to eval() (e.g. eval(f"f'''{template}'''")), any field that allows {...}-style expressions can execute Python.

Vulnerable code (Flask, XML-derived fields):

Regex bypassed: ^[a-zA-Z0-9._'\"(){}=+/]+$ — allows {, }, (, ), ', ", ., etc., so a value like {open("/root/root.txt").read()} passes validation and is then evaluated inside the f-string.

Exploit XML (file read):

RCE variant:


Other Engines Quick Reference

ERB (Ruby)

TrustedSec - Ruby ERB Template Injectionarrow-up-right | PayloadsAllTheThings - Ruby SSTIarrow-up-right

Template syntax: <%= expression %> — evaluates Ruby and outputs the result.

RCE

File Read

If the file does not exist, File.open raises an exception — the app may return a 500 / Internal Server Error. Use this to confirm whether files exist.

Introspection

Enumerate available methods and instance variables to understand the app context:

Reverse Shell via ERB

Wrap the command in backticks inside ERB tags:

Bypassing Input Validation with Newline Injection

When a Ruby/Sinatra app validates input with a regex like =~ /^[a-zA-Z0-9\/ ]+$/, the ^ and $ anchors match per-line, not the entire string. Injecting a URL-encoded newline (%0A) before the SSTI payload puts the malicious content on a new line that passes validation.

Vulnerable regex pattern (Ruby =~ with ^$):

Ruby's ^ matches start of any line and $ matches end of any line. Compare with \A (start of string) and \z (end of string) which would block this bypass.

Bypass — URL-encode \n + SSTI payload after valid input:

Decoded, the server sees:

The regex matches history on line 1 and <%= 7 * 7 %> never hits the check because =~ already matched the first line. The template engine then evaluates the entire string including the SSTI payload.

File read with newline bypass (URL-encoded):

RCE with newline bypass (URL-encoded):

Use urlencoder.orgarrow-up-right to encode payloads when Burp's encoder is not available.

Reference: Bypassing Regular Expression Checksarrow-up-right — covers why ^$ differs from \A\z in Ruby.

Vulnerable Code Pattern (Sinatra + ERB.new)

The root cause is user input concatenated directly into an ERB.new() template string:

When params[:category1] contains ERB tags (after bypassing the regex), the template engine evaluates them. Look for this pattern in Ruby/Sinatra apps where ERB.new takes a string built with user input.

Freemarker (Java)

Pebble (Java)


Resources

Last updated