
Lab Docker reproduisant la chaîne RCE pre-auth de FOSSBilling (contournement d'authentification CVE-2026-27604 + SSTI Twig CVE-2026-28496) avec un PoC Python et une cible de comparaison corrigée.
Ce dépôt contient un laboratoire Docker local permettant de reproduire et de valider la chaîne RCE pré-auth FOSSBilling, composée de deux vulnérabilités enchaînées :
| CVE | Type | CVSS v4 | GHSA | Description |
|---|---|---|---|---|
| CVE-2026-27604 | Contournement d'authentification | 10.0 | GHSA-78x5-c8gw-8279 | Absence de throw dans le vérificateur de rôle de l'API exposant les points de terminaison d'administration aux appelants non authentifiés |
| CVE-2026-28496 | SSTI | 9.4 | GHSA-57mv-jm88-66jc | Rendu de template Twig non sandboxé via l'API string_render |
FOSSBilling est une plateforme de facturation et de gestion client libre et open-source. Les versions 0.5.4 à 0.7.2 sont affectées. FOSSBilling 0.8.0 corrige les deux vulnérabilités.
Ce laboratoire compare deux versions de FOSSBilling :
| Service | Version de FOSSBilling | Objectif | URL |
|---|---|---|---|
| vuln | 0.7.2 | Cible vulnérable | http://localhost:8081 |
| patched | 0.8.0 | Cible corrigée | http://localhost:8082 |
La chaîne validée dans ce laboratoire local est :```text 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}
La cible corrigée (0.8.0) renvoie :```json
{"result":null,"error":{"message":"Unknown API call system/system/string_render","code":879}}
Le lab est intentionnellement limité aux services Docker locaux. Il ne cible pas de systèmes externes et n'inclut pas de webshells, de malwares, de persistance, de callbacks externes, de dump de base de données ni de charges utiles destructrices.
La RCE pré-authentification nécessite que les deux vulnérabilités fonctionnent ensemble :```text ┌──────────────────────────────────────────────────────────────┐ │ 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 │ └──────────────────────────────────────────────────────────────┘
Ce lab démontre la chaîne en utilisant la preuve arithmétique sûre `{{ 7*7 }}`. Le chemin complet de RCE via `getDi()` n'est pas démontré.
## Faits vérifiés
| Affirmation | Preuve | Comment vérifier |
| ----- | -------- | ------------- |
| CVE-2026-27604 est un contournement d'authentification dans la gestion des rôles de l'API FOSSBilling. | GHSA-78x5-c8gw-8279 : un throw manquant dans le vérificateur de rôle permet à /api/system/ de se résoudre en tant qu'admin. | Exécutez le PoC : le chemin invité est refusé, le chemin système renvoie un résultat admin. |
| CVE-2026-28496 est une SSTI dans le rendu Twig de FOSSBilling. | GHSA-57mv-jm88-66jc : string_render passe _tpl dans Twig createTemplate() sans sandbox. | Exécutez le PoC : le serveur évalue {{ 7*7 }} et renvoie 49. |
| Les deux vulnérabilités affectent FOSSBilling 0.5.4 à 0.7.2. | Les avis publics identifient la plage de versions affectées. | Comparez les cibles vulnérable (0.7.2) et corrigée (0.8.0). |
| FOSSBilling 0.8.0 corrige les deux vulnérabilités. | La cible corrigée renvoie « Unknown API call » pour le point de terminaison testé. | Exécutez le PoC contre le port 8082. |
| Le contournement d'authentification donne un accès admin non authentifié. | /api/guest/ refuse string_render ; /api/system/ renvoie le résultat sans authentification. | Exécutez l'étape 1 du PoC. |
| La SSTI évalue des templates contrôlés par l'attaquant. | {{ 7*7 }} renvoie 49 via le chemin vulnérable. | Exécutez l'étape 2 du PoC. |
| La chaîne permet une RCE pré-authentification. | Contournement d'authentification + SSTI = injection de template non authentifiée avec contexte admin. | Exécutez le PoC de la chaîne complète. |
| Le PoC est uniquement HTTP. | poc.py envoie uniquement des requêtes HTTP POST. | Inspectez poc/poc.py. |
## Analyse de la cause racine
### CVE-2026-27604 : Contournement d'authentification
L'API FOSSBilling résout les rôles à partir du chemin de l'URL :```text
/api/:role/:module/:method
Le vérificateur de rôle valide si le rôle demandé est autorisé. Cependant, dans les versions vulnérables, l'exception pour les rôles non autorisés est instanciée mais jamais levée :```php // Simplified vulnerable pattern if (!in_array($role, $allowed_roles)) { new \Exception("Role not allowed"); // BUG: missing "throw" }
Parce que l'exception n'est jamais levée, la validation passe silencieusement. Le rôle `system` se résout en l'identité d'administrateur cron, accordant un accès complet à l'API d'administration à tout appelant non authentifié.
L'impact sur la sécurité :```text
/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)
Le rôle system correspond à l'identité d'administration interne de cron, qui dispose de privilèges administratifs complets.
La méthode d'API d'administration string_render reçoit _tpl à partir des données de la requête et le transmet au pipeline de rendu de template Twig sans application de sandbox :```php
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);
}
La méthode `renderString()` passe à `createTemplateFromString()` :```php
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;
}
Le problème critique : createTemplate($tpl) crée un template Twig à partir de la chaîne contrôlée par l'attaquant et le rend sans restrictions de sandbox. Le template a accès aux objets du contexte du template, y compris le gestionnaire d'API invité qui expose getDi().
Input: POST /api/system/system/string_render {"_tpl":"{{ 7*7 }}"}