Jinja2 was Python. Twig is PHP. The vulnerability is the same — the path looks completely different. Part 3 of the SSTI series.

Last two parts were all about Jinja2 — Python under the hood, sandbox escapes, climbing through object chains. This part is different.

Same vulnerability. Different engine. Different language entirely.

Twig is a template engine for PHP. So everything we did in Jinja2 — the __class__, the __builtins__, the whole Python object chain — none of that applies here. PHP works differently, Twig works differently, and the payloads reflect that completely.

But the core idea hasn’t changed. User input is landing inside the template instead of being passed as a value. The engine is evaluating it. We’re going to use that.

First — What Does Twig Even Tell Us?

Before going for the kill, let’s see what information we can squeeze out of Twig’s own objects.

Twig has a _self keyword that refers to the current template. Inject it:

{{ _self }}

That long string is basically just an internal identifier for the template object. Not particularly useful on its own — you can’t walk it like we walked Python objects in Jinja2. Twig simply doesn’t expose that kind of depth through _self.

So information disclosure in Twig is limited compared to Jinja2. Fine. We move straight to what actually matters.

Reading Files

Here’s where Twig gets interesting. Twig on its own doesn’t have a built-in way to read files. But when Twig runs inside a PHP framework — and a very common one is Symfony — the framework adds its own custom filters on top of Twig. One of those filters is file_excerpt.

And file_excerpt reads files.

{{ "/etc/passwd"|file_excerpt(1,-1) }}

The pipe | is Twig's filter syntax — you're passing the string "/etc/passwd" through the file_excerpt filter, telling it to read from line 1 to the end (-1).

Every user on the system. Same result we got in Jinja2, completely different path to get there. No object chain, no sandbox escape — just a framework filter that wasn’t supposed to be reachable from user input.

This is worth remembering: when you’re testing Twig, you’re not just testing Twig. You’re testing whatever framework it’s running inside. And that framework might be adding functionality that opens doors Twig alone wouldn’t.

Running Commands

Now the fun part. In PHP you can execute system commands with functions like system(). The question is how to call them from inside a Twig template.

Twig has a filter function that applies a callback to a value. And PHP functions are valid callbacks. So you can do this:

{{ ['id'] | filter('system') }}

What’s happening here: you’re passing an array containing the string 'id' through the filter function, using PHP's system as the callback. Twig calls system('id'). PHP executes it.

Command executed. The Array at the end is just PHP printing the return value of filter — ignore it. What matters is uid=33(www-data) right there in the response.

From here you can swap id for anything — cat /etc/shadow, whoami, ls /var/www/html. Whatever the web server user has permission to run, you can run it.

Jinja2 vs Twig — The Same Vulnerability, Twice

Looking at both parts together, something becomes clear.

In Jinja2 we had to climb through Python’s entire object hierarchy just to reach __builtins__ and get to import. It took a loop, a specific class, multiple chained attributes. The payload was long and required understanding how Python's object model works.

In Twig, command execution was one line. {{ ['id'] | filter('system') }}. Done.

PHP and Python handle execution differently, and the constraints each template engine imposes reflect that. Twig is more permissive by default about calling PHP functions. Jinja2 restricts direct access to Python’s dangerous functionality, which is why we needed the sandbox escape.

The vulnerability is identical. The exploitation path depends entirely on the language and engine underneath.

That’s the real lesson from these two parts — understanding the underlying language matters as much as knowing the injection exists. When you find SSTI somewhere unfamiliar, the first thing you do is read the documentation for that engine and its framework. The payloads follow from understanding how the language works, not from memorizing a cheat sheet.

Though cheat sheets like PayloadsAllTheThings do exist and are genuinely useful once you understand what you’re looking for.

Next part — SSTImap, the tool that automates all of this. And then prevention.


SSTI Exploitation in Twig: Same Idea, Different Language was originally published in System Weakness on Medium, where people are continuing the conversation by highlighting and responding to this story.