
Detector del estado de parche basado en comportamiento para Citrix NetScaler CVE-2026-8452. Envía solicitudes SAML diseñadas para determinar si la comprobación del tamaño de PrefixList está presente, sin explotar ni corromper la memoria.
Una comprobación segura y no destructiva del estado del parche para CVE-2026-8452, el desbordamiento de montón previo a la autenticación en el canonizador de firmas SAML de Citrix NetScaler ADC / NetScaler Gateway (CTX696604, CVSS 8.8). Un PrefixList demasiado grande de canonicalización exclusiva desborda un búfer de tamaño fijo durante la canonicalización, que NetScaler realiza antes de validar la firma que lo transporta — por lo que toda la ruta es alcanzable sin credenciales, sin sesión y sin firma válida. Reportado por Michael Tucker del equipo XOR de JPMorgan Chase; el análisis de causa raíz y de explotación se atribuye a watchTowr Labs.
Este script no explota el fallo ni corrompe memoria. Responde una única pregunta por objetivo: ¿está presente el parche en este appliance? — determinada conductualmente, observando el parche en lugar de adivinar la compilación.
Sí. Está diseñado para uso en producción y en evaluaciones:
PrefixListPrefixList. Inflar otros campos más allá de él — URLs del servicio de consumidor de aserciones, nombres de emisor, identificadores de algoritmo, valores de resumen y de firma — no cambia nada en una compilación parcheada, por lo que aplicar el parche no debería causar que una configuración SAML que funciona empiece a fallar.Si modificas el sondeo, no cambies
PROBE_PREFIXESni barras longitudes. 575 bytes es un valor crítico. Otras longitudes dePrefixListpueden desestabilizar un appliance, en al menos un caso en una compilación que incluye este parche, por lo que un barrido de longitudes no es una forma segura de explorar este fallo, y una longitud menor no es más segura.
Las compilaciones parcheadas rechazan un PrefixList demasiado grande de forma limpia, con un mensaje distintivo. Las compilaciones sin parchear atraviesan el analizador y devuelven un error interno genérico. Una misma solicitud idéntica, dos respuestas diferentes:
PrefixList de 575 bytes | Respuesta |
|---|---|
| Sin parchear | 500 Internal Server Error 43549 |
| Parcheado | 200 Malformed Assertion sent to Netscaler |
Se prueban dos rutas, primero IdP, deteniéndose en cuanto una da una respuesta. Cualquiera de ellas es suficiente por sí sola, y juntas cubren ambos roles SAML:
| Ruta | Solicitud | Requiere |
|---|---|---|
| 1 (primera) | POST /saml/login — AuthnRequest firmada, PrefixList en ds:SignedInfo | una política SAML IdP vinculada al vserver objetivo |
| 2 (respaldo) | POST /cgi/samlauth — SAMLResponse, PrefixList en la firma de la aserción | un servicio de consumidor de aserciones SAML SP en el vserver objetivo |
La ruta IdP va primero porque es la más robusta de las dos. Es insensible al valor de Issuer, a AssertionConsumerServiceURL y a la desviación de reloj — un IssueInstant muy fuera de la tolerancia de desviación del appliance sigue discriminando correctamente, porque la canonicalización precede tanto a la comprobación de tiempo como a la de firma.
La
AuthnRequestde la ruta 1 debe estar firmada. Una sin firmar devuelve200 Malformed Assertion sent to Netscaleren compilaciones parcheadas y sin parchear, lo cual es byte-idéntico a la señal de parcheado, por lo que un sondeo que omita el bloque de firma reporta todo appliance como parcheado. La firma no necesita ser válida, y la de esta herramienta no lo es; solo tiene que estar presente, porque suSignedInfoes lo que lleva elPrefixListal canonizador.
Ambas ramas soportadas cambian su comportamiento exactamente en la compilación con el parche, en ambas rutas:
| Compilación | Veredicto | |
|---|---|---|
13.1-63.16 | última 13.1 vulnerable | VULNERABLE |
13.1-63.18 | primera 13.1 parcheada | PATCHED |
14.1-66.59 | 14.1 vulnerable | VULNERABLE |
14.1-72.61 | primera 14.1 parcheada | PATCHED |
13.1-63.16 y 63.18 son versiones consecutivas, por lo que el cambio es atribuible al propio parche y no a la deriva entre compilaciones intermedias.
Esas son las compilaciones donde este parche apareció por primera vez, y el sondeo detecta exactamente esa transición. Ya no son las compilaciones a las que actualizar: boletines posteriores las han superado, por lo que 13.1-63.18 y 14.1-72.61 responden PATCHED aquí pero siguen expuestas a problemas más recientes. Consulta Remediación para las compilaciones parcheadas actuales.
Porque no puede funcionar con este fallo, ni siquiera en principio. 13.1-63.16 y 13.1-63.18, las compilaciones a cada lado del parche, sirven archivos tmindex.html, base.css y resources.js byte-idénticos — el parche no toca ningún activo web. Los hashes de activos estáticos también coinciden entre ramas, por lo que un enfoque basado en hashes puede resolver un appliance vulnerable a una compilación parcheada y reportarlo como limpio, que es el peor modo de fallo que puede tener una herramienta de detección. Por lo tanto, la identificación por huella de la compilación está deliberadamente no implementada. El estado del parche proviene del sondeo, o de show ns version cuando se tienen credenciales.
./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
Dirija la herramienta al **servidor virtual Gateway o AAA**, no a la interfaz de administración. La condición previa es por servidor virtual, por lo que un appliance con varias VIPs necesita que se pruebe cada una.
### Options
| Flag | Description |
| --- | --- |
| `URL` | Uno o más objetivos `https://HOST[:PORT]` |
| `-f, --targets-file FILE` | Lee los objetivos de un archivo (uno por línea; comentarios `#`) |
| `-b, --brief` | Una sola línea alineada por objetivo — veredicto, objetivo, etiqueta de motivo — para escanear muchos hosts |
| `--json` | Emite resultados JSON estructurados |
| `--no-color` | Deshabilita la salida coloreada (también respeta `NO_COLOR` y entornos sin TTY) |
| `--timeout SECS` | Timeout por solicitud (predeterminado: 15) |
### Examples
**Un appliance sin parchear,** respondió en la ruta IdP y se confirmó contra el control:```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 línea de control merece una lectura detallada: la solicitud de 35 bytes supera la comprobación de tamaño
y luego se rechaza por su IssueInstant obsoleto, mientras que la sonda de 575 bytes nunca llegó
tan lejos. Ese es el orden en el que se basa todo el método: la canonicalización se ejecuta antes
de la comprobación de tiempo, igual que se ejecuta antes de la comprobación de firma.
Un dispositivo parcheado, la misma solicitud contra la compilación corregida. Solo la línea de sonda difiere: el
PrefixList de gran tamaño se rechaza por nombre en lugar de caer en el error interno:```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]
**Retrocediendo a la ruta SP.** Aquí el endpoint de IdP es alcanzable pero no hay ninguna política de IdP vinculada a este servidor virtual, por lo que la ruta 1 se abstiene y la ruta 2 responde. Cuando *ninguna* ruta coincide con una política, el veredicto es `INCONCLUSIVE` etiquetado como `no-policy-match` — nunca `PATCHED`, que es la razón principal por la que existe ese veredicto:```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
El control lo detectó. Aquí el endpoint respondió con el mensaje parcheado en ambas longitudes, por lo que la comprobación de tamaño nunca se ejecutó y la respuesta aparentemente concluyente se retira. Esta es la activación del guardián de falsos positivos, y la razón por la que se activó se indica en lugar de dejar que se infiera:```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.
**Escaneo de una flota** (`--brief`), una línea alineada por objetivo que termina en la etiqueta de motivo. El estado de salida es
`1` si algún objetivo es `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
Salida legible por máquina (--json), que registra cada ruta intentada. verdict, reason y detail son los campos autoritativos; attempts es la evidencia bruta, por lo que un intento individual puede indicar patched en un objetivo cuyo veredicto es 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
Every verdict carries a short `reason` tag naming the condition behind it. `--brief` prints the tag as
its third column, and `--json` carries it as `reason`.
| Verdict | Reason tag | Meaning |
| --- | --- | --- |
| `VULNERABLE` | `size-check-absent` | The size check is absent. This appliance is unpatched — patch it. |
| `PATCHED` | `size-check-present` | The size check fired on the code path the probe reached. **Scoped to this CVE:** it does not mean the appliance is on a current build. |
| `UNAFFECTED` | `no-saml-endpoint` | No SAML endpoint answered on this virtual server, so the vulnerable path is not reachable here. **Per-vserver, not per-appliance:** SAML may be configured on another vserver or VIP on the same box. |
| `INCONCLUSIVE` | `flat-response` | The endpoint answered the 575-byte probe and the 35-byte control identically, so the size check was never exercised. The decisive-looking answer is withdrawn — this is the false-positive guard firing. |
| `INCONCLUSIVE` | `no-policy-match` | A SAML endpoint answered but no bound policy matched the probe, so neither route reached the canonicalizer. |
| `INCONCLUSIVE` | `other-saml-error` | A recognized but non-diagnostic SAML condition rejected the probe before the size check — a different length limit, a signature policy, a timestamp. |
| `INCONCLUSIVE` | `unrecognized-reply` | A SAML surface answered with something outside the recognized set. |
| `ERROR` | `not-identified` | Not identified as a NetScaler, or unreachable. |
All four `INCONCLUSIVE` reasons mean the same thing for decision-making — **unknown, not patched.**
Confirm with `show ns version`. The tag exists to tell an operator *which* condition to fix before
re-running: point the probe at a different virtual server, or bind a matching policy.
`INCONCLUSIVE` exists as a distinct verdict, with its own exit code, because a vulnerable appliance can
decline to answer the probe. If the SAML policy bound to a virtual server does not match the probe's
request, the appliance short-circuits ahead of the canonicalizer and returns nothing diagnostic. A
scanner that merely fails to match goes silent on such a host, and silence reads as "patched". This
tool reports it as unknown instead.
### Every verdict is confirmed against a control
`PATCHED` and `VULNERABLE` both rest on a *single* distinguishing response, so the tool verifies that the
response actually depends on what was sent. After a decisive answer it repeats the request with a
short 35-byte `PrefixList` — below any size check — and the verdict stands only if the two answers differ.
If they match, the endpoint replies the same way whatever it receives, the size check was never exercised,
and the result is `INCONCLUSIVE`.
This is not hypothetical. A service provider configured with `samlRejectUnsignedAssertion STRICT` rejects
the probe for a missing signature *before* canonicalization, and answers with the patched message at every
length. Without the control, such an appliance reports `PATCHED` with exit 0 — observed on a genuinely
vulnerable build. It now reports `INCONCLUSIVE` with the reason tag `flat-response`, and the run states
in as many words that the size check was never exercised. The same control catches the mirror-image
case, where an endpoint returns the generic internal error to requests it never parsed.
### Recognized non-diagnostic answers
A NetScaler SAML endpoint has a large set of possible replies, and only two of them establish patch
state. The tool recognizes 20 of the others and names the condition on the per-route line rather than
echoing a response body, for example:```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
Tres de ellos son límites de longitud — un cuerpo POST de tamaño excesivo, un RelayState sobredimensionado, un nombre de
usuario extraído de longitud excesiva. Esos son los que más importan, porque significan que la solicitud fue rechazada por una
comprobación de longitud diferente antes de llegar a la que distingue el estado del parche. Eso normalmente significa que la
sonda debe apuntarse a un servidor virtual diferente, no que el dispositivo esté bien.
Uno de los 20 aparece en cada ejecución de la ruta IdP: la respuesta de control message timestamp outside the appliance's skew tolerance. La sonda lleva un IssueInstant fijo, por lo que una respuesta de control que supera la
comprobación de tamaño es rechazada después por su antigüedad — una propiedad de la sonda, no del dispositivo. Una respuesta
fuera del conjunto de 20 se imprime como su propia primera línea en lugar de una condición nombrada.
Todos ellos siguen produciendo INCONCLUSIVE, etiquetado como other-saml-error. Reconocer una respuesta nunca
la promueve a PATCHED: solo una respuesta explícita de build parcheado hace eso, y toda respuesta no reconocida
cae también en INCONCLUSIVE — como unrecognized-reply. El reconocimiento existe para decirle a un
operador por qué un objetivo no pudo clasificarse, no para clasificarlo.
| Código | Significado |
|---|---|
0 | Parcheado, o no afectado en el servidor virtual objetivo |
1 | Al menos un objetivo es VULNERABLE |
2 | Error de uso (argumentos incorrectos / archivo de objetivos ilegible) |
3 | Al menos un objetivo es INCONCLUSIVE, ninguno vulnerable |
4 | Al menos un objetivo dio error, ninguno vulnerable ni inconcluso |
2 es el código de salida propio de argparse para una invocación incorrecta, por lo que los códigos de veredicto lo omiten. Un script contenedor
puede distinguir así «este dispositivo no pudo clasificarse» (3) de «invoqué mal la herramienta» (2),
algo que un esquema que sobrecargara 2 no podría.
En un barrido de múltiples objetivos, el código se elige por prioridad, no por el peor estado:
VULNERABLE > INCONCLUSIVE > ERROR > limpio. Un host inalcanzable, por tanto, nunca enmascara un
hallazgo vulnerable en el código de salida.
PATCHED y el código de salida 0 significan que la
comprobación de tamaño de CVE-2026-8452 está presente en la ruta que alcanzó la sonda. No dicen nada sobre ninguna otra
vulnerabilidad de NetScaler, incluidas las divulgadas después de este fallo y corregidas en builds posteriores. No
leas el código de salida 0 de esta herramienta como un certificado de buena salud para un dispositivo.UNAFFECTED significa «no alcanzable aquí», no «este dispositivo es seguro»./saml/login aplica a todo el dispositivo, pero en un servidor virtual sin
ninguna política IdP vinculada responde Matching policy not found y cortocircuita antes del
canonicalizador. Un IdP cuya política usa una regla de expresión que la solicitud de la sonda no cumple
terminará en INCONCLUSIVE en lugar de dar una respuesta.INCONCLUSIVE no es un certificado de buena salud. Se distingue deliberadamente de PATCHED
con un código de salida separado para que el silencio nunca se confunda con un resultado aprobado.INCONCLUSIVE. Cuando la política SAML se encuentra
en una etiqueta de políticas alcanzada por nextFactor en lugar de estar vinculada directamente al servidor virtual, una
aserción no solicitada no encuentra ninguna política coincidente y cortocircuita antes del canonicalizador.
Verificado en un build vulnerable, que devolvió INCONCLUSIVE. Dado que SAML detrás de un primer factor de postura de dispositivo
o de esquema de inicio de sesión es un patrón común, trata INCONCLUSIVE en un gateway nFactor como
«probablemente alcanzable, confirma con show ns version», no como una curiosidad.VULNERABLE confirma la ausencia de la comprobación de tamaño, que es el
estado del parche. No mide hasta dónde podría explotar un atacante la corrupción en tu build.Actualiza a 13.1-63.21 o posterior, o 14.1-73.32 o posterior (FIPS y NDcPP: 14.1-73.32 FIPS, o 13.1-37.277 para 13.1-FIPS y 13.1-NDcPP).
La corrección de CVE-2026-8452 en sí se incluyó por primera vez en 13.1-63.18 / 14.1-72.61 según
CTX696604,
y esa es la transición que detecta esta herramienta. Esos builds han sido superados desde entonces por
CTX696939
(2026-08-19), que añade CVE-2026-19489 y CVE-2026-19490, esta última un bypass de autenticación
previo a la autenticación con CVSS 9.3. Su condición previa en builds desde 14.1-43.56 / 13.1-61.28
en adelante es una acción SAML configurada — por lo que un dispositivo dentro del alcance del fallo que comprueba esta
herramienta probablemente también lo esté para ese, y un veredicto PATCHED aquí no es razón para aplazar la actualización. Ambos
boletines quedan cubiertos por los builds indicados arriba.
Los dispositivos en 12.1 o 13.0 no tienen corrección y no recibirán ninguna — esas ramas están al final de su vida útil y deben tratarse como permanentemente vulnerables y migrarse a una rama con soporte.
Dos notas adicionales:
Parchea ambos nodos de un par HA. Un secundario sin parchear es un dispositivo totalmente expuesto en el momento en que asume el control.
Acota tu inventario por configuración SAML, no por tipo de servidor virtual. La redacción del aviso del proveedor
(servidor virtual Gateway o AAA) es más amplia que la condición de activación. Revisa en la configuración en ejecución
la presencia de add authentication samlAction y add authentication samlIdPProfile junto con
add authentication vserver y add vpn vserver.
Esto está probado, no inferido. En un dispositivo confirmado como vulnerable, eliminamos todos los objetos SAML,
vinculamos un factor de autenticación no SAML en su lugar y dejamos los servidores virtuales AAA activos y sirviendo:
los endpoints SAML respondieron entonces 404 a todas las solicitudes. No están simplemente restringidos por políticas sin configuración SAML — no existen. Por lo tanto, un servidor virtual sin SAML está genuinamente fuera del alcance de
este fallo, y UNAFFECTED en un objetivo así es una respuesta real, no un punto ciego. La salvedad que
sigue aplicándose es la del alcance mencionada arriba: es por servidor virtual, así que confirma cada VIP en lugar de
concluir nada sobre el dispositivo.
CVE-2026-8452 se publicó junto a cinco vulnerabilidades hermanas en el mismo boletín. La que vale la pena vigilar además es CVE-2026-8451, una sobrelectura de memoria previa a la autenticación en la ruta SAML IdP que ha sido objeto de explotación activa en el mundo real. Ambas comparten una superficie de ataque, por lo que la misma auditoría de configuración cubre ambas.
Este código se distribuye bajo una licencia MIT.
El uso de esta herramienta para atacar objetivos sin consentimiento mutuo previo es ilegal. Es responsabilidad del usuario final cumplir todas las leyes locales, estatales y federales aplicables. Los desarrolladores no asumen ninguna responsabilidad y no son responsables de ningún uso indebido o daño causado por este programa.