Une appliance VPN entièrement corrigée, et pourtant root
Voici tout le premier mouvement : une requête POST vers la page de connexion publique d’Ivanti Connect Secure, welcome.cgi, qui ne nécessite aucune session, avec un en-tête supplémentaire, X-DSCGI-PATH, pointant vers le chemin d’un autre fichier CGI normalement authentifié. Le contrôle d’authentification lit l’URL. L’en-tête décide où la requête va réellement. [1]
Volexity suit l’acteur derrière cela sous le nom UTA0576, et au moment où son analyse technique a été publiée le 13 octobre 2026, le groupe avait déjà utilisé cette astuce d’en-tête, enchaînée à un second bogue, pour obtenir une exécution de code non authentifiée en tant qu’utilisateur cgi-server sans privilèges de l’appliance, sur un build intégralement corrigé, 22.8R2.4. [1] Ces deux bogues sont suivis conjointement sous CVE-2026-104935 : CVSS 8,6, CWE-290 (contournement d’authentification par usurpation), touchant Ivanti Connect Secure 25.1.0.0 avant 25.1.1.2, 25.1.2.0 avant 25.1.3.0 et tous les builds 22.8R2 jusqu’à 22.8R2.4, corrigé dans 25.1.1.2, 25.1.3.0, 25.1.3.1 et 22.8R2.5 le 13 octobre 2026. [2] La CISA l’a ajoutée le jour même au catalogue des vulnérabilités activement exploitées, avec une échéance de trois jours pour les agences fédérales, déjà dépassée. [2][3]
L’exécution de code en tant que cgi-server n’est pas la fin de l’histoire. Volexity a également reconstitué une troisième étape, toujours sans numéro de CVE ni correctif, qui mène de là jusqu’au root, sans porte dérobée ni webshell. Je n’ai moi-même rien exécuté de tout cela ; ce qui suit s’appuie sur l’analyse technique de Volexity et sur les fiches CVE et KEV, consultées le 18 octobre 2026. [1][2][3] Ce qui mérite votre attention n’est pas qu’une appliance VPN ait été compromise. C’est que deux des trois bogues sont exactement le genre de choses qu’un relecteur de code repère, pas le genre que découvre un intervenant en réponse à incident après coup.
POST /dana-na/auth/url_default/welcome.cgi HTTP/1.1
Host: vpn.example.com
X-DSCGI-PATH: /home/webserver/htdocs/dana/fb/smb/wu.cgi
Content-Type: application/x-www-form-urlencoded
(welcome.cgi needs no session at all)
// Host and paths are illustrative, not turnkey.
// The URL says "public login page" -> the auth check reads the URL.
// The header says "run wu.cgi instead" -> routing reads the header.
// Nothing checks that the two agree.La page de connexion vérifie l’URL. Un en-tête décide quel script s’exécute
Tout dispatcher de ce type répond à deux questions distinctes pour chaque requête : cet appelant est-il autorisé à être là, et quel fichier la traite réellement. ICS répond à la première en vérifiant le chemin de l’URL, c’est-à-dire s’il s’agit de l’un des points d’entrée publics non authentifiés, et à la seconde en vérifiant un en-tête de routage, X-DSCGI-PATH, qui nomme le script CGI à invoquer. [1] Séparer cela en deux questions relève d’une ingénierie raisonnable. Rien dans cette conception n’est, en soi, négligent.
Elle se brise uniquement parce que rien ne vérifie que les deux réponses concordent. Le dispatcher suppose qu’une requête qui atteint le script de connexion public est, par définition, une requête publique. L’attaquant contrôle un en-tête qui redéfinit silencieusement quel script la traite réellement. Pointez cet en-tête vers wu.cgi, un gestionnaire de téléversement protégé, et une requête qui ressemble à un accès à la page de connexion devient en réalité un appel en territoire authentifié, s’exécutant avec toute la confiance que le dispatcher accorde à ce qu’il croit déjà avoir franchi la porte.
Le gestionnaire de téléversement fait confiance à l’horloge, pas au contenu
Être routé vers wu.cgi est la première étape. La seconde transforme ce routage en exécution de code, et il s’agit d’un bogue de timing, pas de contenu. wu.cgi écrit un fichier téléversé dans /home/runtime/tmp/<MD5 du nom de fichier que vous avez choisi>, et ce n’est qu’une fois le fichier sur disque que le gestionnaire vérifie si le contenu est bien une archive ZIP valide. Si ce n’est pas le cas, il supprime le fichier. [1] Nommez un script Perl avec une extension .zip, et pendant une courte fenêtre, un script prêt à s’exécuter volontiers se trouve à un chemin que vous connaissez déjà, puisque vous avez choisi le nom de fichier et qu’un hash prévisible l’a transformé en ce chemin.
Volexity décrit comment l’attaquant fait courir des requêtes parallèles précisément pendant cette fenêtre : suffisamment de requêtes, routées via l’astuce d’en-tête de la première étape, visant ce chemin temporaire, pour intercepter le script Perl avant que la suppression ne gagne la course. Une seule requête arrivant à temps suffit. [1]
La charge utile récupérée par Volexity ne s’arrêtait pas à Perl. Elle enchaînait vers un script Python, livré compressé en Base64 et zlib puis exécuté en mémoire via exec() plutôt qu’écrit comme fichier à part entière. [1] L’objectif, à chaque étape : laisser le moins de traces possible sur le disque pour qu’on ne les retrouve pas ensuite.
// wu.cgi's order of operations, reconstructed from Volexity's writeup.
// This is the sequence, not a working exploit.
1. Attacker uploads "payload.zip" containing a Perl script, not a real ZIP.
2. wu.cgi writes it to /home/runtime/tmp/<MD5("payload.zip")> immediately.
3. wu.cgi then checks: is this actually a valid ZIP archive?
4. Check fails -> wu.cgi deletes the file.
// The gap between step 2 and step 4 is the whole bug.
// The attacker fires repeated requests (via the stage-one header trick)
// at the path from step 2, racing to execute it before step 4 wins.
// One request landing inside that window is enough.Deux étapes corrigées. La troisième ne l’est pas, et c’est la plus intéressante
Le correctif d’Ivanti pour CVE-2026-104935, publié le 13 octobre 2026, referme les étapes un et deux : 25.1.1.2, 25.1.3.0, 25.1.3.1 et 22.8R2.5 sont les versions où cette chaîne précise cesse de fonctionner. [2] Si vous faites tourner un build antérieur sur l’une de ces trois lignes, ce n’est pas un point à « programmer pour le prochain sprint ». Les propres données d’exploitation de la fiche CVE indiquent déjà « active », avec un impact technique « total », et l’échéance fédérale de trois jours fixée par la CISA est déjà derrière nous. [2][3]
C’est à la troisième étape que s’arrêtent les notes de correctif. Une fois qu’un attaquant dispose d’une exécution de code en tant qu’utilisateur cgi-server sans privilèges, Volexity a constaté qu’UTA0576 redétournait dsinvoked, le propre service de maintenance root d’ICS, pour exécuter du code choisi par l’attaquant en tant que root, en redirigeant la cible d’une tâche déjà existante, entièrement dans la mémoire d’un processus déjà en cours d’exécution. [1] Je ne détaillerai pas la bibliothèque précise ni les détails de configuration cités par Volexity. Le mécanisme est public ; un correctif ne l’est pas, et répéter ici des détails de niveau exploit pour une escalade encore ouverte n’est pas quelque chose que je fais, quel que soit celui qui l’a déjà publié.
Ce qui compte, indépendamment du statut du correctif : l’escalade s’annule elle-même. Elle restaure l’entrée de configuration et les octets de la bibliothèque en mémoire une fois le root confirmé, en vérifiant si /etc/shadow est lisible, de sorte que l’appareil paraisse intact ensuite. Volexity affirme sans détour que l’Integrity Checker Tool d’Ivanti elle-même ne détecte pas cela, car ICT vérifie ce qui se trouve sur le disque, et ceci ne touche jamais le disque. [1] Un passage ICT propre aujourd’hui ne dit rien sur le fait que la troisième étape ait touché un appareil, ni sur le fait que cela se soit produit avant le 13 octobre ou puisse encore se produire maintenant, puisqu’il n’existe toujours aucun correctif pour elle.
Comment je chercherais cela dans du code n’ayant rien à voir avec Ivanti
Les étapes un et deux ne sont pas des erreurs spécifiques à ICS. Ce sont deux motifs que je vérifie dans tout reverse proxy, toute passerelle d’API ou tout dispatcher de type CGI, sur n’importe quelle pile technique. Premièrement : le composant qui décide si un appelant est autorisé lit-il le même champ que celui qui décide quel gestionnaire s’exécute ? Si l’autorisation se fonde sur l’URL et le routage sur un en-tête, un cookie ou un paramètre de requête que la couche d’authentification ne regarde jamais, ce même bogue existe ici aussi, avec simplement des noms de champs différents.
Deuxièmement : existe-t-il quelque chose que votre service écrit vers un chemin référençable, ne serait-ce que brièvement, avant d’avoir été validé ? Les gestionnaires de téléversement en sont le terrain classique, mais cela apparaît partout où un service persiste d’abord et vérifie ensuite : une écriture en cache, un fichier temporaire, une entrée de file qu’un worker peut récupérer avant qu’un validateur n’ait tourné. Les deux questions ci-dessous sont celles que je poserais réellement en lisant pour la première fois un gestionnaire comme wu.cgi, et elles n’ont pas besoin du code source d’Ivanti pour s’appliquer.
// Two questions I ask reading any dispatcher or upload handler,
// independent of what language or framework it's written in.
// 1. Auth-vs-routing mismatch
// - What field decides "is this caller allowed in"? (often the URL)
// - What field decides "which handler actually runs"? (sometimes a
// header, a cookie, a query parameter)
// - If those are two different fields, does anything enforce that
// they agree?
// 2. Persist-before-validate
// - Where does a handler write attacker-influenced content to a
// referenceable path (disk, cache, queue) before fully validating it?
// - Is there a window, however small, where something worse can
// reach what was just written before cleanup or validation runs?Est-ce votre urgence ? Si vous exploitez Connect Secure, oui
Soyons honnêtes sur qui est concerné. Si vous n’exploitez pas Ivanti Connect Secure, passez votre chemin ; cela ne vous concerne pas. Si vous l’exploitez, il n’y a pas ici d’atterrissage en douceur comme c’est parfois le cas avec une faille de faible gravité gonflée en urgence. CVE-2026-104935 est pré-authentification, ses propres données d’exploitation indiquent déjà « active », et la reconstitution forensique de Volexity a révélé qu’elle avait touché une appliance sur laquelle tous les correctifs disponibles étaient déjà appliqués. [1][2] « Nous sommes à jour » était vrai pour ce client, et cela ne l’a pas sauvé.
Trois éléments déterminent le niveau d’inquiétude. D’abord, le build réellement exécuté sur l’appareil à l’instant présent, et non celui que votre journal des modifications indique comme cible visée : 25.1.1.1 et antérieurs, 25.1.2.2 et antérieurs, et 22.8R2.4 et antérieurs restent tous exposés aux étapes un et deux. [2] Ensuite, le correctif de CVE-2026-104935 marque-t-il la fin de votre évaluation du risque ou son début. Il referme la porte par laquelle UTA0576 est passé, pas celle que Volexity a trouvée déjà utilisée une fois à l’intérieur. Enfin, un résultat propre de l’Integrity Checker Tool tient-il lieu de preuve d’absence de compromission, ce que l’analyse même de Volexity dément explicitement pour cette chaîne. [1]
Le score de 8,6 est honnête concernant les étapes un et deux. Il ne dit rien de la troisième, puisque cette dernière n’a pas encore de score, et un attaquant qui enchaîne les trois ne cesse pas de s’en soucier simplement parce que le CVSS, lui, s’arrête là.
Ce qu’il faut vérifier avant de considérer l’affaire close
Corriger en premier, évidemment. Mais corriger après coup répond à « sommes-nous en sécurité désormais », pas à « avons-nous déjà été touchés », et la fenêtre d’exploitation de cette chaîne ne vous imposait d’être non corrigé d’aucune manière que vous auriez remarquée sur le moment. [1]
Volexity retrace l’infrastructure de l’attaquant jusqu’au 30 septembre 2026, avec une exploitation observée à partir du 3 octobre. [1] Si l’appliance a fait tourner un build exposé à un moment quelconque depuis fin septembre 2026, traitez-la comme potentiellement compromise et recherchez des traces avant de clore le dossier. La liste ci-dessous suit l’ordre dans lequel je procéderais.
Before you call this closed:
[ ] Confirm the build actually RUNNING on the appliance, not the one
your change log says. 25.1.1.2 / 25.1.3.0 / 25.1.3.1 / 22.8R2.5 or
later closes CVE-2026-104935 (stages one and two only).
[ ] Pull logs for requests to welcome.cgi where X-DSCGI-PATH does not
match the URL path. That mismatch is this chain's fingerprint.
[ ] Do not close the incident on a clean Integrity Checker Tool run
alone. ICT checks on-disk state; stage three never touches disk.
[ ] If the appliance was internet-facing on a vulnerable build any time
from late September 2026 on, treat it as possibly compromised, not
just unpatched.
[ ] Track Ivanti's advisory for the still-missing privilege-escalation
fix and apply it the day it ships. There is no CVE for it yet.La CVE est la partie facile
CVE-2026-104935 elle-même disparaîtra bientôt de l’actualité. Les deux classes de bogues qui la sous-tendent, non, car elles n’ont jamais été propres à Ivanti seul. Un contrôle d’autorisation et une décision de routage qui font silencieusement confiance à deux champs différents d’une même requête, et un chemin d’écriture qui persiste avant de valider : je retrouve les deux dans des bases de code web et API qui n’ont jamais touché à une appliance VPN, le plus souvent à des endroits que personne n’a regardés deux fois parce que le contrôle évident — cet endpoint est-il authentifié, ce type de téléversement est-il vérifié — avait déjà été franchi.
C’est la frontière qu’une revue de code sécurité est conçue pour tracer : non pas si une requête a atteint une fonction, mais si chaque porte sur son chemin lit les mêmes faits concernant cette requête. Si vous voulez que quelqu’un relise votre couche d’authentification et de routage avec cette question en tête avant qu’elle ne devienne le rapport de zero-day de quelqu’un d’autre, c’est exactement le travail que je fais. Dites-moi ce que vous exploitez et nous cadrerons la mission.
Sources
Besoin de vérifier ce point dans votre propre produit ?
Revue de code sécuritéVous voulez apprendre à trouver vous-même ce type de vulnérabilité ?
Formation individuelle à la revue de code sécurisé