
Détecteur comportemental de l'état du correctif pour Citrix NetScaler CVE-2026-8452. Envoie des requêtes SAML spécialement conçues pour déterminer si la vérification de la taille de PrefixList est présente, sans exploiter ni corrompre la mémoire.
Une vérification sûre et non destructive de l'état du correctif pour CVE-2026-8452, le débordement de tas pré-authentification
dans le canonicaliseur de signature SAML de Citrix NetScaler ADC / NetScaler Gateway
(CTX696604,
CVSS 8.8). Un PrefixList de canonicalisation exclusive surdimensionné déborde un tampon de taille fixe lors de la
canonicalisation, que NetScaler effectue avant de valider la signature qui le transporte — donc le
chemin complet est accessible sans identifiants, sans session et sans signature valide. Signalé par Michael
Tucker de l'équipe XOR de JPMorgan Chase ; l'analyse de la cause racine et de l'exploitation est due à
watchTowr Labs.
Ce script n'exploite pas la faille et ne corrompt pas la mémoire. Il répond à une seule question par cible : le correctif est-il présent sur cette appliance ? — déterminé par le comportement, en observant le correctif plutôt qu'en devinant le build.
Oui. Il est conçu pour une utilisation en production et en évaluation :
PrefixListPrefixList. Gonfler d'autres champs au-delà — URL du service de consommateur d'assertion,
noms d'émetteur, identifiants d'algorithme, valeurs de digest et de signature — ne change rien sur un build corrigé, donc
l'application du correctif ne devrait pas faire échouer une configuration SAML fonctionnelle.Si vous modifiez la sonde, ne changez pas
PROBE_PREFIXESet ne balayez pas les longueurs. 575 octets est déterminant. D'autres longueurs dePrefixListpeuvent déstabiliser une appliance, dans au moins un cas sur un build qui porte ce correctif, donc un balayage de longueurs n'est pas un moyen sûr d'explorer cette faille et une longueur plus courte n'est pas plus sûre.
Les builds corrigés rejettent proprement un PrefixList surdimensionné, avec un message distinctif. Les builds non corrigés
passent à travers le parseur et renvoient une erreur interne générique. Une seule requête identique, deux réponses
différentes :
PrefixList de 575 octets | Réponse |
|---|---|
| Non corrigé | 500 Internal Server Error 43549 |
| Corrigé | 200 Malformed Assertion sent to Netscaler |
Deux routes sont essayées, IdP en premier, en s'arrêtant dès que l'une donne une réponse. Chacune est suffisante seule, et ensemble elles couvrent les deux rôles SAML :
| Route | Requête | Prérequis |
|---|---|---|
| 1 (première) | POST /saml/login — AuthnRequest signé, PrefixList dans ds:SignedInfo | une politique IdP SAML liée au vserver ciblé |
| 2 (repli) | POST /cgi/samlauth — SAMLResponse, PrefixList dans la signature d'assertion | un service consommateur d'assertion SP SAML sur le vserver ciblé |
La route IdP passe en premier car elle est la plus robuste des deux. Elle est insensible à la valeur Issuer,
à l'AssertionConsumerServiceURL, et à la dérive d'horloge — un IssueInstant bien en dehors de la
tolérance de dérive de l'appliance discrimine toujours correctement, car la canonicalisation précède la vérification
de temps ainsi que la vérification de signature.
L'
AuthnRequestde la route 1 doit être signé. Une requête non signée renvoie200 Malformed Assertion sent to Netscalersur les builds corrigés et non corrigés, ce qui est identique octet pour octet au signal des builds corrigés, donc une sonde qui omet le bloc de signature rapporte chaque appliance comme corrigée. La signature n'a pas besoin d'être valide, et celle de cet outil ne l'est pas ; elle doit seulement être présente, car sonSignedInfoest ce qui transporte lePrefixListdans le canonicaliseur.
Les deux branches supportées changent de comportement exactement au build de correctif, sur les deux routes :
| Build | Verdict | |
|---|---|---|
13.1-63.16 | dernier 13.1 vulnérable | VULNERABLE |
13.1-63.18 | premier 13.1 corrigé | PATCHED |
14.1-66.59 | 14.1 vulnérable | VULNERABLE |
14.1-72.61 | premier 14.1 corrigé | PATCHED |
13.1-63.16 et 63.18 sont des versions consécutives, donc le changement est attribuable au correctif lui-même
plutôt qu'à une dérive entre les builds intermédiaires.
Ce sont les builds où ce correctif est apparu en premier, et la sonde détecte exactement cette transition.
Ils ne sont plus les builds vers lesquels effectuer la mise à niveau : des bulletins ultérieurs les ont remplacés, donc 13.1-63.18 et
14.1-72.61 répondent tous deux PATCHED ici tout en restant exposés à des problèmes plus récents. Voir
Remédiation pour les builds corrigés actuels.
Parce que cela ne peut pas fonctionner sur cette faille, même en principe. 13.1-63.16 et 13.1-63.18, les builds
immédiatement de part et d'autre du correctif, servent tmindex.html, base.css et
resources.js identiques octet pour octet — le correctif ne touche aucun actif web. Les hash d'actifs statiques entrent également en collision entre les branches, donc une
approche basée sur les hash peut faire correspondre une appliance vulnérable à un build corrigé et la rapporter comme propre,
ce qui est le pire mode de défaillance pour un outil de détection. L'empreinte du build est donc délibérément
non implémentée. L'état du correctif provient de la sonde, ou de show ns version si vous disposez des
identifiants.
./cve_2026_8452_check.py https://gateway.example.com
./cve_2026_8452_check.py https://gateway.example.com:9443
./cve_2026_8452_check.py -f targets.txt --brief
./cve_2026_8452_check.py -f targets.txt --json > results.json
Pointez l'outil vers la **passerelle ou le serveur virtuel AAA**, et non vers l'interface de gestion. La
condition préalable s'applique par serveur virtuel, donc une appliance avec plusieurs VIP doit être testée sur chacun d'eux.
### Options
| Indicateur | Description |
| --- | --- |
| `URL` | Une ou plusieurs cibles `https://HOST[:PORT]` |
| `-f, --targets-file FILE` | Lire les cibles depuis un fichier (une par ligne ; commentaires `#`) |
| `-b, --brief` | Une ligne alignée par cible — verdict, cible, étiquette de raison — pour analyser de nombreux hôtes |
| `--json` | Émettre des résultats JSON structurés |
| `--no-color` | Désactiver la sortie colorée (respecte aussi `NO_COLOR` et les non-TTY) |
| `--timeout SECS` | Délai d'expiration par requête (défaut : 15) |
### Exemples
**Une appliance non corrigée,** ayant répondu sur la route IdP et confirmée par rapport au témoin :```console
$ ./cve_2026_8452_check.py https://gateway.example.com:9443
====================================================================
CVE-2026-8452 - NetScaler SAML PrefixList patch-state check
https://gateway.example.com:9443
====================================================================
>> Identifying the appliance
[ OK ] NetScaler indicators: 5 (CSP contains citrixng://)
>> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
idp /saml/login HTTP 500 / 43549: no size check present
idp /saml/login 35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check
[FAIL] Size check absent (via the IDP route)
====================================================================
RESULT: VULNERABLE
====================================================================
https://gateway.example.com:9443 via IDP [size-check-absent]
The size check is absent. This appliance is unpatched for
CVE-2026-8452. Upgrade to 13.1-63.21+ / 14.1-73.32+ (12.1 and
13.0 are EOL and never fixed).
====================================================================
La ligne de contrôle mérite d'être lue de près : la requête de 35 octets passe le contrôle de taille
et est ensuite rejetée pour son IssueInstant obsolète, tandis que la sonde de 575 octets n'est jamais
arrivée jusque-là. C'est sur cet ordre que repose toute la méthode — la canonicalisation s'exécute avant
le contrôle temporel, tout comme elle s'exécute avant le contrôle de signature.
Un équipement corrigé, la même requête contre la version corrigée. Seule la ligne de sonde diffère — le
PrefixList surdimensionné est rejeté par son nom au lieu de retomber sur l'erreur interne :```console
Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control) idp /saml/login HTTP 200 "Malformed Assertion": size check rejected the probe idp /saml/login 35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check [ OK ] Size check present (via the IDP route)
RESULT: PATCHED
https://vpn.example.com via IDP [size-check-present]
**Repli sur la route SP.** Ici, le point de terminaison IdP est joignable mais aucune politique IdP n'est liée à
ce serveur virtuel, donc la route 1 s'abstient et la route 2 répond. Lorsque *aucune* des routes ne correspond à une politique,
le verdict est `INCONCLUSIVE` étiqueté `no-policy-match` — jamais `PATCHED`, ce qui est précisément la raison pour laquelle
ce verdict existe :```console
>> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
idp /saml/login HTTP 200 "Matching policy not found": parser not reached
sp /cgi/samlauth HTTP 500 / 43549: no size check present
sp /cgi/samlauth 35-byte control: HTTP 200 "assertion rejected before the size check": a different SAML condition, not the size check
[FAIL] Size check absent (via the SP route)
RESULT: VULNERABLE
Le contrôle l'a détecté. Ici, l'endpoint a répondu avec le message corrigé aux deux longueurs, de sorte que la vérification de taille n'a jamais été exercée et que la réponse qui semblait décisive est retirée. C'est le garde-fou contre les faux positifs qui se déclenche, et la raison de son déclenchement est explicitée plutôt que laissée à l'inférence :```console
Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control) idp /saml/login HTTP 200 "Malformed Assertion": size check rejected the probe idp /saml/login 35-byte control: HTTP 200 "Malformed Assertion": size check rejected the probe [WARN] Probe and control answered alike, so the size check was never exercised
RESULT: INCONCLUSIVE
https://sp-strict.example.com via IDP [flat-response]
The 575-byte probe and the 35-byte control got the same answer, so this endpoint replies the same way whatever it is sent and the size check was never exercised. Unknown, not patched.
**Scan d'une flotte** (`--brief`), une ligne alignée par cible se terminant par la balise de raison. Le code de sortie est
`1` si une cible est `VULNERABLE` :```console
$ ./cve_2026_8452_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE https://gateway.example.com:9443 size-check-absent
VULNERABLE https://gateway.example.com:9444 size-check-absent
PATCHED https://vpn.example.com size-check-present
INCONCLUSIVE https://sp-strict.example.com flat-response
INCONCLUSIVE https://gw-nopolicy.example.com no-policy-match
UNAFFECTED https://mgmt.example.com no-saml-endpoint
ERROR https://offline.example.com not-identified
exit: 1
Sortie lisible par machine (--json), qui enregistre chaque route tentée. verdict, reason et
detail sont les champs faisant autorité ; attempts est la preuve brute, de sorte qu'une tentative individuelle peut
indiquer patched sur une cible dont le verdict est INCONCLUSIVE :```console
$ ./cve_2026_8452_check.py https://vpn.example.com --json
[
{
"target": "https://vpn.example.com",
"verdict": "PATCHED",
"reason": "size-check-present",
"route": "idp",
"detail": "HTTP 200 "Malformed Assertion": size check rejected the probe",
"attempts": [
{
"route": "idp",
"path": "/saml/login",
"state": "patched",
"detail": "HTTP 200 "Malformed Assertion": size check rejected the probe"
},
{
"route": "idp",
"path": "/saml/login",
"state": "control:known-error",
"detail": "35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check"
}
],
"netscaler_indicators": [
"CSP contains citrixng://",
"CSP contains com.citrix.nsgclient://",
"CSP contains nsgcepa://",
"CSP report-uri /nscsp_violation/report_uri",
"/vpn/js/rdx/ present (HTTP 404)"
]
}
]
## Verdicts
Chaque verdict porte une courte étiquette `reason` nommant la condition qui le sous-tend. `--brief` affiche l'étiquette en tant que
troisième colonne, et `--json` la porte comme `reason`.
| Verdict | Étiquette de raison | Signification |
| --- | --- | --- |
| `VULNERABLE` | `size-check-absent` | La vérification de taille est absente. Cet appliance n'est pas patché — patchez-le. |
| `PATCHED` | `size-check-present` | La vérification de taille s'est déclenchée sur le chemin de code atteint par la sonde. **Limitée à cette CVE :** cela ne signifie pas que l'appliance est sur une version à jour. |
| `UNAFFECTED` | `no-saml-endpoint` | Aucun endpoint SAML n'a répondu sur ce serveur virtuel, donc le chemin vulnérable n'est pas accessible ici. **Par vserver, pas par appliance :** SAML peut être configuré sur un autre vserver ou VIP sur la même machine. |
| `INCONCLUSIVE` | `flat-response` | L'endpoint a répondu de manière identique à la sonde de 575 octets et au témoin de 35 octets, donc la vérification de taille n'a jamais été exercée. La réponse à l'apparence décisive est retirée — c'est le garde-fou anti-faux positifs qui se déclenche. |
| `INCONCLUSIVE` | `no-policy-match` | Un endpoint SAML a répondu mais aucune politique liée ne correspondait à la sonde, donc aucun des deux chemins n'a atteint le canonicaliseur. |
| `INCONCLUSIVE` | `other-saml-error` | Une condition SAML reconnue mais non diagnostique a rejeté la sonde avant la vérification de taille — une autre limite de longueur, une politique de signature, un horodatage. |
| `INCONCLUSIVE` | `unrecognized-reply` | Une surface SAML a répondu avec quelque chose en dehors de l'ensemble reconnu. |
| `ERROR` | `not-identified` | Non identifié comme un NetScaler, ou inaccessible. |
Les quatre raisons `INCONCLUSIVE` signifient la même chose pour la prise de décision — **inconnu, non patché.**
Confirmez avec `show ns version`. L'étiquette existe pour indiquer à un opérateur *quelle* condition corriger avant
de relancer : pointer la sonde vers un autre serveur virtuel, ou lier une politique correspondante.
`INCONCLUSIVE` existe en tant que verdict distinct, avec son propre code de sortie, car un appliance vulnérable peut
refuser de répondre à la sonde. Si la politique SAML liée à un serveur virtuel ne correspond pas à la
requête de la sonde, l'appliance court-circuite avant le canonicaliseur et ne renvoie rien de diagnostique. Un
scanner qui échoue simplement à correspondre devient silencieux sur un tel hôte, et le silence se lit comme « patché ». Cet
outil le signale plutôt comme inconnu.
### Chaque verdict est confirmé contre un témoin
`PATCHED` et `VULNERABLE` reposent tous deux sur une *seule* réponse distinctive, l'outil vérifie donc que la
réponse dépend réellement de ce qui a été envoyé. Après une réponse décisive, il répète la requête avec un
`PrefixList` court de 35 octets — en dessous de toute vérification de taille — et le verdict ne tient que si les deux réponses diffèrent.
Si elles correspondent, l'endpoint répond de la même manière quoi qu'il reçoive, la vérification de taille n'a jamais été exercée,
et le résultat est `INCONCLUSIVE`.
Ce n'est pas hypothétique. Un fournisseur de services configuré avec `samlRejectUnsignedAssertion STRICT` rejette
la sonde pour une signature manquante *avant* la canonicalisation, et répond avec le message patché à chaque
longueur. Sans le témoin, un tel appliance rapporte `PATCHED` avec le code de sortie 0 — observé sur un build réellement
vulnérable. Il rapporte désormais `INCONCLUSIVE` avec l'étiquette de raison `flat-response`, et l'exécution déclare
en toutes lettres que la vérification de taille n'a jamais été exercée. Le même témoin détecte le cas en miroir,
où un endpoint renvoie l'erreur interne générique à des requêtes qu'il n'a jamais analysées.
### Réponses non diagnostiques reconnues
Un endpoint SAML NetScaler dispose d'un large ensemble de réponses possibles, et seulement deux d'entre elles établissent l'état de patch. L'outil reconnaît 20 des autres et nomme la condition sur la ligne par route plutôt que de renvoyer un corps de réponse, par exemple :```text
idp /saml/login HTTP 200 "post body over the appliance's maximum": a different length limit rejected the probe first
sp /cgi/samlauth HTTP 200 "assertion rejected before the size check": a different SAML condition, not the size check
Trois d'entre elles sont des limites de longueur — un corps de requête POST surdimensionné, un RelayState surdimensionné, un nom d'utilisateur extrait trop long. Ce sont les plus importantes, car elles signifient que la requête a été rejetée par une vérification de longueur différente avant d'atteindre celle qui distingue l'état du correctif. Cela signifie généralement que la sonde doit être pointée vers un autre serveur virtuel, et non que l'appliance est saine.
Une de ces 20 apparaît à chaque exécution sur la route IdP : le message timestamp outside the appliance's skew tolerance du contrôle. La sonde transporte un IssueInstant fixe, si bien qu'un contrôle qui passe la vérification de taille est ensuite rejeté en raison de son âge — une propriété de la sonde, et non de l'appliance. Une réponse hors de l'ensemble des 20 est imprimée sur sa propre première ligne au lieu d'une condition nommée.
Toutes produisent encore INCONCLUSIVE, avec la balise other-saml-error. Reconnaître une réponse ne la fait jamais passer à PATCHED : seule une réponse explicite indiquant un build corrigé fait cela, et toute réponse non reconnue retombe également dans INCONCLUSIVE — sous la forme unrecognized-reply. La reconnaissance existe pour indiquer à un opérateur pourquoi une cible n'a pas pu être classifiée, et non pour la classifier.
| Code | Signification |
|---|---|
0 | Corrigé, ou non affecté sur le serveur virtuel ciblé |
1 | Au moins une cible est VULNERABLE |
2 | Erreur d'utilisation (arguments invalides / fichier de cibles illisible) |
3 | Au moins une cible est INCONCLUSIVE, aucune n'est vulnérable |
4 | Au moins une cible a généré une erreur, aucune n'est vulnérable ni inconclusive |
2 est le code de sortie propre à argparse pour un appel invalide, c'est pourquoi les codes de verdict l'évitent. Un script d'encapsulation peut donc distinguer « cette appliance n'a pas pu être classifiée » (3) de « j'ai mal appelé l'outil » (2), ce qu'un schéma qui surchargeait 2 ne permettrait pas.
Lors d'un balayage multi-cibles, le code est choisi par priorité, et non par le pire statut : VULNERABLE > INCONCLUSIVE > ERROR > sain. Un hôte injoignable ne masque donc jamais un résultat vulnérable dans le code de sortie.
PATCHED et le code de sortie 0 signifient que la vérification de taille de la CVE-2026-8452 est présente sur le chemin atteint par la sonde. Ils ne disent rien sur toute autre vulnérabilité NetScaler, y compris celles divulguées après ce bug et corrigées dans des builds ultérieurs. Ne considérez pas le code de sortie 0 de cet outil comme un satisfecit pour une appliance.UNAFFECTED signifie « non joignable ici », et non « cette appliance est sûre »./saml/login est global à l'appliance, mais sur un serveur virtuel sans politique IdP liée, il répond Matching policy not found et court-circuite avant le canonicalizer. Un IdP dont la politique utilise une règle d'expression que la requête de la sonde ne satisfait pas se retrouvera dans INCONCLUSIVE plutôt que de donner une réponse.INCONCLUSIVE n'est pas un satisfecit. Il est délibérément distingué de PATCHED par un code de sortie séparé, afin qu'un silence ne soit jamais pris pour un résultat positif.INCONCLUSIVE. Lorsque la politique SAML se trouve dans une étiquette de politique atteinte par nextFactor plutôt que liée directement au serveur virtuel, une assertion non sollicitée ne trouve aucune politique correspondante et court-circuite avant le canonicalizer. Vérifié sur un build vulnérable, qui a rapporté INCONCLUSIVE. Comme la SAML derrière un premier facteur de posture d'appareil ou de schéma de connexion est une configuration courante, traitez INCONCLUSIVE sur une passerelle nFactor comme « probablement joignable, confirmez avec show ns version », et non comme une curiosité.VULNERABLE confirme l'absence de la vérification de taille, ce qui correspond à l'état du correctif. Il ne mesure pas jusqu'où un attaquant pourrait mener la corruption sur votre build.Mettez à niveau vers 13.1-63.21 ou une version ultérieure, ou 14.1-73.32 ou une version ultérieure (FIPS et NDcPP : 14.1-73.32 FIPS, ou 13.1-37.277 pour 13.1-FIPS et 13.1-NDcPP).
Le correctif de la CVE-2026-8452 elle-même a d'abord été livré dans 13.1-63.18 / 14.1-72.61 selon CTX696604, et c'est cette transition que cet outil détecte. Ces builds ont depuis été supplantés par CTX696939 (2026-08-19), qui ajoute CVE-2026-19489 et CVE-2026-19490, cette dernière étant un contournement d'authentification en phase de pré-authentification (CVSS 9.3). Sa condition préalable sur les builds à partir de 14.1-43.56 / 13.1-61.28 est une action SAML configurée — si bien qu'une appliance dans le périmètre du bug que cet outil vérifie est probablement aussi dans le périmètre de cette dernière, et un verdict PATCHED ici n'est pas une raison de différer la mise à niveau. Les deux bulletins sont résolus par les builds ci-dessus.
Les appliances sous 12.1 ou 13.0 n'ont pas de correctif et n'en recevront pas — ces branches sont en fin de vie et doivent être considérées comme définitivement vulnérables et migrées vers une branche prise en charge.
Deux remarques supplémentaires :
add authentication samlAction et add authentication samlIdPProfile, ainsi que de add authentication vserver et add vpn vserver.Ce point est testé, pas déduit. Sur une appliance confirmée vulnérable, nous avons retiré chaque objet SAML, lié un facteur d'authentification non SAML à la place, et laissé les serveurs virtuels AAA actifs et en service : les points de terminaison SAML ont alors renvoyé 404 à chaque requête. Ils ne sont pas simplement conditionnés par une politique en l'absence de configuration SAML — ils n'existent pas. Un serveur virtuel sans SAML est donc réellement hors du périmètre de ce bug, et UNAFFECTED sur une telle cible est une vraie réponse, et non un angle mort. La réserve qui s'applique toujours est celle du périmètre ci-dessus : elle vaut par serveur virtuel, confirmez donc chaque VIP plutôt que de conclure quoi que ce soit sur l'appliance.
La CVE-2026-8452 a été publiée aux côtés de cinq vulnérabilités sœurs dans le même bulletin. Celle qu'il vaut la peine de suivre en parallèle est CVE-2026-8451, une sur-lecture mémoire en pré-authentification dans le chemin SAML IdP qui a fait l'objet d'une exploitation active dans la nature. Les deux partagent une surface d'attaque, si bien que le même audit de configuration couvre les deux.
Ce code est distribué sous licence MIT.
L'utilisation de cet outil pour attaquer des cibles sans consentement mutuel préalable est illégale. Il incombe à l'utilisateur final de respecter toutes les lois locales, des États et fédérales applicables. Les développeurs déclinent toute responsabilité et ne sont pas responsables des mauvais usages ou des dommages causés par ce programme.