
Docker lab reproducing the FOSSBilling pre-auth RCE chain (CVE-2026-27604 auth bypass + CVE-2026-28496 Twig SSTI) with a Python PoC and patched comparison target.
This repository contains a local Docker lab for reproducing and validating the FOSSBilling Pre-Auth RCE chain, composed of two chained vulnerabilities:
| CVE | Type | CVSS v4 | GHSA | Description |
|---|---|---|---|---|
| CVE-2026-27604 | Auth Bypass | 10.0 | GHSA-78x5-c8gw-8279 | Missing throw in API role checker exposes admin endpoints to unauthenticated callers |
| CVE-2026-28496 | SSTI | 9.4 | GHSA-57mv-jm88-66jc | Unsandboxed Twig template rendering via string_render API |
FOSSBilling is a free and open-source billing and client management platform. Versions 0.5.4 through 0.7.2 are affected. FOSSBilling 0.8.0 patches both vulnerabilities.
This lab compares two FOSSBilling versions:
| Service | FOSSBilling version | Purpose | URL |
|---|---|---|---|
| vuln | 0.7.2 | Vulnerable target | http://localhost:8081 |
| patched | 0.8.0 | Patched target | http://localhost:8082 |
The validated chain in this local lab is:
Unauthenticated HTTP POST
→ /api/system/system/string_render
→ Role "system" resolves to cron admin identity (CVE-2026-27604)
→ _tpl={{ 7*7 }} passed into unsandboxed Twig rendering (CVE-2026-28496)
→ Server evaluates the template expression
→ Returns {"result":"49","error":null}
The patched target (0.8.0) returns:
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
The lab is intentionally scoped to local Docker services. It does not target external systems and does not include web shells, malware, persistence, external callbacks, database dumping, or destructive payloads.
The Pre-Auth RCE requires both vulnerabilities working together:
┌──────────────────────────────────────────────────────────────┐
│ STEP 1: Auth Bypass (CVE-2026-27604) │
│ │
│ URL path: /api/system/system/string_render │
│ Role "system" → cron admin identity │
│ Exception instantiated but never thrown │
│ → Unauthenticated caller gets admin API access │
├──────────────────────────────────────────────────────────────┤
│ STEP 2: SSTI (CVE-2026-28496) │
│ │
│ Admin API method: System\Api\Admin::string_render() │
│ _tpl parameter → Twig createTemplate() → render() │
│ No sandbox enforcement │
│ → Server-side template evaluation │
├──────────────────────────────────────────────────────────────┤
│ COMBINED: Pre-Auth RCE │
│ │
│ One unauthenticated HTTP POST │
│ → Admin access (auth bypass) │
│ → Template injection (SSTI) │
│ → getDi() exposes Pimple DI container │
│ → PDO, cache, extension manager, 40+ services reachable │
│ → Remote Code Execution │
└──────────────────────────────────────────────────────────────┘
This lab demonstrates the chain using the safe arithmetic proof {{ 7*7 }}. The full RCE path via getDi() is not demonstrated.
| Claim | Evidence | How to verify |
|---|---|---|
| CVE-2026-27604 is an auth bypass in FOSSBilling API role handling. | GHSA-78x5-c8gw-8279: missing throw in role checker allows /api/system/ to resolve as admin. | Run the PoC: guest path is denied, system path returns admin result. |
| CVE-2026-28496 is an SSTI in FOSSBilling Twig rendering. | GHSA-57mv-jm88-66jc: string_render passes _tpl into Twig createTemplate() without sandbox. | Run the PoC: server evaluates {{ 7*7 }} and returns 49. |
| Both vulnerabilities affect FOSSBilling 0.5.4 through 0.7.2. | Public advisories identify the affected version range. | Compare vuln (0.7.2) and patched (0.8.0) targets. |
| FOSSBilling 0.8.0 patches both vulnerabilities. | Patched target returns "Unknown API call" for the tested endpoint. | Run the PoC against port 8082. |
| The auth bypass gives unauthenticated admin access. | /api/guest/ denies string_render; /api/system/ returns the result without auth. | Run Stage 1 of the PoC. |
| The SSTI evaluates attacker-controlled templates. | {{ 7*7 }} returns 49 through the vulnerable path. | Run Stage 2 of the PoC. |
| The chain enables Pre-Auth RCE. | Auth bypass + SSTI = unauthenticated template injection with admin context. | Run the full chain PoC. |
| The PoC is HTTP-only. | poc.py sends HTTP POST requests only. | Inspect poc/poc.py. |
The FOSSBilling API resolves roles from the URL path:
/api/:role/:module/:method
The role checker validates whether the requested role is allowed. However, in vulnerable versions, the exception for disallowed roles is instantiated but never thrown:
// Simplified vulnerable pattern
if (!in_array($role, $allowed_roles)) {
new \Exception("Role not allowed"); // BUG: missing "throw"
}
Because the exception is never thrown, the validation silently passes. The role system resolves to the cron admin identity, granting full admin API access to any unauthenticated caller.
The security impact:
/api/guest/system/string_render → denied (guest role, no admin access)
/api/admin/system/string_render → requires authentication
/api/system/system/string_render → admin access WITHOUT authentication (bypass)
The system role maps to the internal cron admin identity, which has full administrative privileges.
The string_render admin API method receives _tpl from request data and passes it into the Twig template rendering pipeline without sandbox enforcement:
public function string_render($data)
{
if (!isset($data['_tpl'])) {
error_log('_tpl parameter not passed');
return '';
}
$tpl = $data['_tpl'];
$try_render = $data['_try'] ?? false;
$vars = $data;
unset($vars['_tpl'], $vars['_try']);
return $this->getService()->renderString($tpl, $try_render, $vars);
}
The renderString() method falls through to createTemplateFromString():
public function createTemplateFromString($tpl, $try_render, $vars)
{
try {
$twig = $this->di['twig'];
$template = $twig->createTemplate($tpl);
$parsed = $template->render($vars);
} catch (\Exception $e) {
$parsed = $tpl;
if (!$try_render) {
throw $e;
}
}
return $parsed;
}
The critical issue: createTemplate($tpl) creates a Twig template from the attacker-controlled string and renders it without sandbox restrictions. The template has access to objects in the template context, including the guest API handler which exposes getDi().
Input: POST /api/system/system/string_render {"_tpl":"{{ 7*7 }}"}
Step 1 (CVE-2026-27604):
URL path → role = "system"
→ role checker: exception instantiated, NOT thrown
→ system role → cron admin identity
→ admin API access granted without authentication
Step 2 (CVE-2026-28496):
Admin::string_render() → reads _tpl from request
→ Service::renderString() → createTemplateFromString()
→ Twig createTemplate("{{ 7*7 }}")
→ Twig evaluates the expression
→ returns "49"
Full RCE path (not demonstrated in this safe PoC):
{{ guest.getDi() }}
→ returns the Pimple DI container
→ PDO, cache, password hashing, extension manager, 40+ services
→ SQL execution, credential extraction, code execution
FOSSBilling 0.8.0 addresses both vulnerabilities: