
Detecta de forma segura el bypass de autenticación SAML de Citrix NetScaler CVE-2026-19490
Una comprobación de vulnerabilidad segura y no autenticada para CVE-2026-19490, el bypass de
autenticación previo a la autenticación en la ruta del proveedor de servicios SAML de Citrix NetScaler ADC / NetScaler Gateway
(CTX696939,
publicado el 2026-08-19). CWE-288, CVSS v4.0 9.3
(AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L). Reportado por Samarth Vashisht del
equipo de pruebas de penetración de JPMorgan Chase.
El dispositivo decodifica en base64 el parámetro RelayState de una respuesta SAML y, cuando el texto plano
comienza con ctx=, entrega el resto al deserializador de contexto nFactor. En una compilación sin parchear, un
fallo de deserialización propaga la longitud del RelayState decodificado como el código de disposición interno
de la solicitud en lugar de un error, por lo que un atacante no autenticado elige qué rama interna
toma el dispositivo a continuación simplemente eligiendo la longitud del RelayState. Algunas ramas generan una sesión
real de Gateway; otras provocan un fallo del motor de paquetes y reinician el dispositivo. Este script no hace ninguna de las dos cosas
— envía la única longitud validada que no crea ninguna sesión y deja en paz al motor de paquetes, y
responde a una única pregunta por objetivo: Un resultado distinto de
.
VULNERABLE# single target
./cve_2026_19490_check.py https://gateway.example.com
# a specific Gateway or AAA virtual server
./cve_2026_19490_check.py https://gateway.example.com:9443
# several targets; the scheme defaults to https://
./cve_2026_19490_check.py gw-a.example.com gw-b.example.com:9443
# scan a list, one target per line ('#' comments allowed), compact output
./cve_2026_19490_check.py -f targets.txt --brief
# machine-readable output for pipelines
./cve_2026_19490_check.py -f targets.txt --json > results.json
Python 3.8+, solo biblioteca estándar — sin paquetes de terceros.
Apunte la herramienta al servidor virtual Gateway o AAA, no a la interfaz de gestión. La exposición es
por servidor virtual, por lo que un dispositivo con varias VIP necesita que se pruebe cada una. La sonda lleva un
estricto envoltorio de seguridad — una única longitud de RelayState validada, nunca un barrido — que
¿Es seguro ejecutarlo? detalla.
| Flag | Descripción |
|---|---|
TARGET | Uno o más objetivos [https://]HOST[:PORT]; el esquema por defecto es https:// |
-f, --targets-file FILE | Leer objetivos desde un archivo (uno por línea; comentarios con #) |
--timeout SECS | Tiempo de espera por solicitud (por defecto: 15) |
--workers N | Objetivos concurrentes (por defecto: 16); la salida mantiene el orden de entrada |
-b, --brief | Una sola línea alineada por objetivo — ideal para escanear muchos hosts |
--json | Emitir JSON estructurado, incluyendo cada solicitud enviada por objetivo |
--no-color | Deshabilitar la salida con color (también respeta NO_COLOR y entornos no TTY) |
Un dispositivo vulnerable (la salida predeterminada de dos líneas). El marcador [!] y VULNERABLE se muestran
en rojo en una TTY:
$ ./cve_2026_19490_check.py https://gateway.example.com:9443
[!] https://gateway.example.com:9443: VULNERABLE [internal-error-43524]
HTTP 500 / 43524: the decoded RelayState length was propagated as the internal error, so the CTX696939 fix is absent
Un dispositivo parcheado:
$ ./cve_2026_19490_check.py https://vpn.example.com
[+] https://vpn.example.com: PATCHED [fixed-error-returned]
HTTP 200 "Malformed Assertion": the fixed error was returned on the path this probe reached, so the CTX696939 fix is present (>= 13.1-63.21 / 14.1-73.32)
La activación de la protección contra falsos positivos. Tanto la sonda como el control de la misma longitud devolvieron la señal sin parchear, por lo que la respuesta no depende de lo que se envió y se retira la respuesta que parecía decisiva:
$ ./cve_2026_19490_check.py https://sp-strict.example.com
[?] https://sp-strict.example.com: INCONCLUSIVE [flat-response]
the probe and the same-length control both answered HTTP 500 / 43524, so the reply does not depend on what was sent and the fix was never exercised; unknown, not patched
Barriendo un parque (--brief). Las dos filas de gateway.example.com son los servidores virtuales SP y solo-IdP
en el mismo dispositivo — ambos responden, algo que una comprobación de precondición de configuración no lograría:
$ ./cve_2026_19490_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE https://gateway.example.com:9443 internal-error-43524
VULNERABLE https://gateway.example.com:9444 internal-error-43524
PATCHED https://vpn.example.com fixed-error-returned
INCONCLUSIVE https://sp-strict.example.com flat-response
UNAFFECTED https://lb.example.com no-saml-endpoint
ERROR https://www.example.com not-identified
exit: 1
Salida legible por máquina (--json). Se incluye cada solicitud, por lo que un hallazgo puede volver a derivarse
de la evidencia en lugar de confiar en él. El control se registra por su relación con la sonda en lugar
de como un veredicto propio, porque un control que se lee como una compilación parcheada es el resultado esperado en
todas las compilaciones:
$ ./cve_2026_19490_check.py https://gateway.example.com:9443 --json
[
{
"target": "https://gateway.example.com:9443",
"verdict": "VULNERABLE",
"reason": "internal-error-43524",
"detail": "HTTP 500 / 43524: the decoded RelayState length was propagated as the internal error, so the CTX696939 fix is absent",
"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)"
],
"attempts": [
{
"kind": "probe",
"path": "/cgi/samlauth",
"status": 500,
"state": "unpatched",
"detail": "HTTP 500 / 43524: the decoded RelayState length was propagated as the internal error"
},
{
"kind": "control",
"path": "/cgi/samlauth",
"status": 200,
"state": "differs-from-probe",
"detail": "same-length control: HTTP 200 \"Malformed Assertion\": the fixed error was returned"
}
]
}
]
Sí. Está diseñado para uso en producción y evaluación:
RelayState de la sonda se decodifica a exactamente 20 bytes y la
herramienta no envía ninguna otra longitud bajo ningún flag. En un dispositivo sin parchear, la longitud decodificada es el
selector de rama, y las ramas incluyen algunas que crean una sesión y otras que provocan un SIGSEGV en el
motor de paquetes — reiniciando todo el NetScaler y descartando el tráfico durante aproximadamente 45 segundos. Veinte
bytes aterrizan en una rama limpia de error interno que no crea ninguna sesión, y el constructor de payload
se niega a construir cualquier otra longitud, por lo que una edición en otro lugar no puede ampliar silenciosamente la sonda.13.1-63.18 sin parchear con cero cores generados y sin reinicio del motor de paquetes. La comprobación en sí
se ha ejecutado desde entonces contra ambas ramas de mantenimiento, en ambos lados del parche, y contra
servidores virtuales de proveedor de servicios y solo-IdP por igual, sin reinicio del motor de paquetes en ninguno
de ellos.SAMLResponse
debe estar presente para que se alcance la rama RelayState, pero su contenido es
irrelevante, por lo que son cuatro bytes de basura. La rama no emite ninguna cookie ni escribe ninguna configuración.VULNERABLE nunca se reporta con una sola respuesta — véase
el control.Si modifica la sonda, no cambie la longitud del
RelayStatedecodificado y no barra longitudes. Veinte bytes es fundamental, y más corto no es más seguro. Otras longitudes no están probadas en el mejor de los casos y son una interrupción del dispositivo en el peor.
Tres solicitudes HTTP por objetivo, y una cuarta solo cuando el dispositivo devuelve la señal sin parchear:
| # | Solicitud | Propósito |
|---|---|---|
| 1 | GET /logon/LogonPoint/tmindex.html | Identificación de NetScaler (tokens de Content-Security-Policy) |
| 2 | GET /vpn/js/rdx/core/rdx.js | Identificación de NetScaler |
| 3 | POST /cgi/samlauth | la sonda |
| 4 | POST /cgi/samlauth | el control, enviado solo tras una señal sin parchear |
No se siguen las redirecciones y nunca se devuelve ninguna cookie — NetScaler sirve un NSC_DLGE caducado
que un cliente que lo reenvía llevaría a la solicitud 4, convirtiendo el POST en una página de inicio de sesión.
nsppe registra el POST SAML en /var/log/ns.log a nivel informativo, sin necesidad de ningún interruptor de depuración, como
un fallo de deserialización de contexto nFactor que incluye la línea
nFactor: ctx deserialize aaa_info, base64 decode failed. No hay líneas de creación de sesión o de inicio de sesión
con esta longitud, que es lo que distingue un escaneo de un intento del bypass.
El parche es una sola instrucción. Ante una deserialización de contexto fallida, la compilación vulnerable lleva la
longitud del RelayState decodificado hacia adelante como el estado de la solicitud; la compilación corregida lleva el error
constante 0xe0005, representado como Malformed Assertion. Así que un solo POST los separa:
| Respuesta a la sonda | Compilación | Veredicto |
|---|---|---|
500 Internal Server Error 43524 | sin parchear | VULNERABLE |
200 Malformed Assertion sent to Netscaler | parcheada | PATCHED |
La compilación parcheada responde Malformed Assertion a cualquier longitud, que es lo que hace que la respuesta
sin parchear sea un oráculo en lugar de una coincidencia.
VULNERABLE se basa en una única respuesta distintiva, por lo que la herramienta verifica que la respuesta realmente
depende de lo que se envió. Tras la señal sin parchear, repite la solicitud con un RelayState de control
de la misma longitud decodificada de 20 bytes, que difiere solo en los 16 bytes posteriores a ctx=. Ese
contenido es rechazado por una guarda anterior al deserializador, por lo que todas las compilaciones probadas — parcheadas y
sin parchear — lo responden con 200 Malformed Assertion. El veredicto se mantiene solo si las dos respuestas difieren.
Si coinciden, algo está devolviendo 500/43524 independientemente de la entrada, y el resultado es
INCONCLUSIVE etiquetado como flat-response en lugar de un falso VULNERABLE. Mantener el control en la
misma longitud decodificada lo mantiene dentro del envoltorio de seguridad validado y aísla el contenido como la única
variable.
La guarda es unidireccional. No puede validar un veredicto PATCHED, porque un dispositivo genuinamente
parcheado responde de forma idéntica a la sonda y al control — eso es lo que significa estar parcheado aquí. Véase
Limitaciones.
La deserialización de RelayState se ejecuta antes de la coincidencia de políticas SAML, por lo que un servidor virtual que lleva solo un
samlIdPPolicy responde a la sonda, no solo uno con un samlAction de proveedor de servicios
vinculado. Eso es una cobertura más amplia que la que puede ofrecer una comprobación de precondición de configuración.
La respuesta vulnerable es la única que se corrobora a sí misma: lleva un marcador específico, y el
control demuestra que la respuesta depende de lo que se envió. PATCHED también es evidencia positiva — el
propio error corregido del dispositivo en la ruta que alcanzó la sonda — pero está acotado a este CVE y a
esa ruta. INCONCLUSIVE y ERROR no son evidencia en absoluto: la sonda nunca alcanzó el
deserializador, por lo que el estado del parche es desconocido, y por eso INCONCLUSIVE es un veredicto
separado en lugar de fusionarse con PATCHED. Reportar el silencio como aprobado se equivoca en la dirección
peligrosa. Donde necesite certeza, confirme con show ns version contra las compilaciones en
Remediación.
Cada veredicto lleva una etiqueta reason corta. --brief la imprime como la tercera columna y --json
la lleva como reason.
| Veredicto | Etiqueta de motivo | Significado |
|---|---|---|
VULNERABLE | internal-error-43524 | El dispositivo propagó la longitud del RelayState decodificado como su error interno, y un control de la misma longitud demostró que la respuesta depende de lo que se envió. El parche CTX696939 está ausente — parchee. |
PATCHED | fixed-error-returned | El dispositivo devolvió el error corregido en la ruta que alcanzó la sonda. Acotado a este CVE, y véase Limitaciones sobre dispositivos interpuestos. |
UNAFFECTED | no-saml-endpoint | /cgi/samlauth devolvió 404, por lo que el consumidor de aserciones SAML no se sirve aquí. Por vserver, no por dispositivo, y véase la advertencia sobre compilaciones antiguas en Limitaciones. |
INCONCLUSIVE | flat-response | La sonda y el control de la misma longitud respondieron de forma idéntica, por lo que la respuesta no depende de lo que se envió. La protección contra falsos positivos se activó. |
INCONCLUSIVE | generic-internal-error | El endpoint devolvió 43549, su error interno genérico, que devuelven tanto las compilaciones parcheadas como las sin parchear. No es el discriminador — y tenga en cuenta que sí es el marcador sin parchear del CVE-2026-8452 no relacionado, por lo que parece un acierto y no lo es. |
INCONCLUSIVE | unrecognized-reply | El endpoint respondió con algo fuera del conjunto reconocido. |
INCONCLUSIVE | no-probe-response | El host respondió a la identificación pero no a la sonda — un tiempo de espera, un reinicio, o un middlebox descartando el POST. Reintente. |
ERROR | not-identified | No identificado como un NetScaler, o inalcanzable. |
Los cuatro motivos de INCONCLUSIVE significan lo mismo para la toma de decisiones — desconocido, no parcheado.
La etiqueta nombra qué condición corregir antes de volver a ejecutar.
| Código | Significado |
|---|---|
0 | Ningún objetivo era VULNERABLE |
1 | Al menos un objetivo es VULNERABLE |
2 | Error de uso (argumentos incorrectos / archivo de objetivos ilegible) |
La salida 0 no es un certificado de buena salud. PATCHED, UNAFFECTED y los cuatro motivos de INCONCLUSIVE
se reducen a ella, y solo uno de esos es un resultado positivo. Un wrapper que necesite distinguir
"parcheado" de "no se pudo clasificar" debe leer el veredicto — primera columna de --brief, o el
campo verdict de --json — no el código de salida.
PATCHED. Malformed Assertion es lo que el
dispositivo devuelve siempre que la sonda no logra alcanzar el deserializador de contexto, y estar parcheado es
solo una de las razones por las que eso ocurre. Un dispositivo interpuesto que elimina, altera o cortocircuita el
parámetro RelayState produce por tanto PATCHED en un dispositivo vulnerable, y ninguna solicitud adicional que
la herramienta pudiera enviar separaría las dos. Donde algo pueda estar delante del
dispositivo, confirme con show ns version.UNAFFECTED está acotado a las compilaciones actuales. En 14.1-43.55 / 13.1-61.27 y anteriores, el
aviso indica que la exposición no requiere ninguna acción SAML en absoluto — cualquier servidor virtual Gateway o AAA
está afectado — por lo que un 404 en tal compilación no deja al dispositivo fuera del alcance. Ese tramo
se toma del aviso en lugar de medirse.PATCHED no dice nada sobre ninguna otra
vulnerabilidad de NetScaler, incluido CVE-2026-19489 en el mismo boletín.VULNERABLE establece que el parche
está ausente en la ruta que alcanzó la sonda. No mide hasta dónde podría llevar un atacante el
bypass en su configuración, y no le dirá si alguien ya lo ha hecho — búsquelo en
/var/log/ns.log por separado.Actualice a 13.1-63.21 o posterior, o 14.1-73.32 o posterior (FIPS y NDcPP: 13.1-37.277 para 13.1-FIPS y 13.1-NDcPP, 14.1-73.32 FIPS para 14.1-FIPS), según CTX696939. Los dispositivos en 12.1 o 13.0 no tienen parche y no recibirán ninguno — esas ramas están al final de su vida útil y deben migrarse a una rama soportada.
Tres notas adicionales:
defaultAuthorizationAction de ningún vpn sessionAction vinculado al vserver — recurre al
set vpn parameter -defaultAuthorizationAction global. El valor predeterminado de fábrica es DENY; ALLOW
es común en el campo porque es a lo que recurren los administradores en lugar de escribir políticas de
autorización por usuario, y ALLOW es lo que permite que la sesión no autenticada alcance recursos
internos. Es global, no por vserver. Esto acota el impacto; no es un parche.add authentication samlAction y add authentication samlIdPProfile junto con
add authentication vserver y add vpn vserver — sujeto a la advertencia sobre compilaciones antiguas anterior.CTX696939 también corrige CVE-2026-19489, y esta superficie se solapa en gran medida con CVE-2026-8452 del boletín anterior: en compilaciones desde 14.1-43.56 / 13.1-61.28 en adelante, ambos problemas están condicionados a una acción SAML configurada, por lo que una auditoría de configuración delimita ambos.
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 obedecer todas las leyes locales, estatales y federales aplicables. Los desarrolladores no asumen ninguna responsabilidad y no son responsables de ningún mal uso o daño causado por este programa.