
Rileva in modo sicuro il bypass di autenticazione SAML di Citrix NetScaler CVE-2026-19490
Un controllo di vulnerabilità sicuro e non autenticato per CVE-2026-19490, il bypass di
autenticazione pre-autenticazione nel percorso del service provider SAML di Citrix NetScaler ADC / NetScaler Gateway
(CTX696939,
pubblicato il 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). Segnalato da Samarth Vashisht del
team di penetration testing di JPMorgan Chase.
L'appliance decodifica in base64 il parametro RelayState di una risposta SAML e, quando il testo in chiaro
inizia con ctx=, passa il resto al deserializzatore del contesto nFactor. Su una build non patchata un
fallimento di deserializzazione propaga la lunghezza del RelayState decodificato come codice di
disposizione interno della richiesta invece di un errore, così un attaccante non autenticato sceglie quale ramo
interno l'appliance prende successivamente semplicemente scegliendo quanto è lungo il RelayState. Alcuni rami generano una vera
sessione Gateway; altri mandano in crash il packet engine e riavviano l'appliance. Questo script non fa né l'una né l'altra
cosa — invia l'unica lunghezza validata per non creare alcuna sessione e non toccare il packet engine, e
risponde a una sola domanda per target: Un risultato diverso da
.
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 libreria standard — nessun pacchetto di terze parti.
Punta lo strumento al Gateway o al virtual server AAA, non all'interfaccia di gestione. L'esposizione è
per virtual server, quindi un'appliance con diversi VIP richiede che ciascuno venga testato. La sonda porta con sé un
rigido involucro di sicurezza — un'unica lunghezza RelayState validata, mai scansionata a tappeto — che
È Sicuro Eseguirlo? illustra.
| Flag | Descrizione |
|---|---|
TARGET | Uno o più target [https://]HOST[:PORT]; lo schema predefinito è https:// |
-f, --targets-file FILE | Legge i target da un file (uno per riga; commenti #) |
--timeout SECS | Timeout per richiesta (predefinito: 15) |
--workers N | Target concorrenti (predefinito: 16); l'output resta nell'ordine di input |
-b, --brief | Una singola riga allineata per target — ideale per scansionare molti host |
--json | Emette JSON strutturato, includendo ogni richiesta inviata per target |
--no-color | Disabilita l'output colorato (rispetta anche NO_COLOR e non-TTY) |
Un'appliance vulnerabile (l'output predefinito su due righe). Il marcatore [!] e VULNERABLE vengono
resi in rosso su un 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'appliance patchata:
$ ./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 guardia anti-falsi positivi che scatta. La sonda e il controllo della stessa lunghezza hanno entrambi restituito il segnale non patchato, quindi la risposta non dipende da ciò che è stato inviato e la risposta dall'aspetto decisivo viene ritirata:
$ ./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
Scansione di un parco macchine (--brief). Le due righe gateway.example.com sono i virtual server SP e
solo-IdP sulla stessa appliance — entrambi rispondono, cosa che un controllo di precondizione di configurazione non
riuscirebbe a fare:
$ ./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
Output leggibile dalla macchina (--json). Ogni richiesta è inclusa, così un risultato può essere ri-derivato
dalle prove anziché essere preso per buono. Il controllo è registrato per la sua relazione con la sonda
piuttosto che come un verdetto a sé stante, perché un controllo che si legge come una build patchata è il risultato atteso
su ogni build:
$ ./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ì. È progettato per l'uso in produzione e in fase di assessment:
RelayState della sonda si decodifica in esattamente 20 byte e lo
strumento non invia nessun'altra lunghezza con nessun flag. Su un'appliance non patchata la lunghezza decodificata è il
selettore di ramo, e i rami includono quelli che creano una sessione e quelli che mandano in SIGSEGV il
packet engine — riavviando l'intero NetScaler e facendo cadere il traffico per circa 45 secondi. Venti
byte atterrano su un ramo di errore interno pulito che non crea alcuna sessione, e il costruttore del payload
rifiuta di costruire qualsiasi altra lunghezza, così una modifica altrove non può silenziosamente ampliare la sonda.13.1-63.18 non patchata con zero core generati e nessun riavvio del packet engine. Il controllo stesso
è stato eseguito da allora contro entrambi i rami di manutenzione, su entrambi i lati della correzione, e contro
virtual server service-provider e solo-IdP allo stesso modo, senza alcun riavvio del packet engine su nessuno
di essi.SAMLResponse
deve essere presente perché il ramo RelayState venga raggiunto, ma il suo contenuto è
irrilevante, quindi sono quattro byte di spazzatura. Il ramo non emette alcun cookie e non scrive alcuna configurazione.VULNERABLE non viene mai segnalato su una singola risposta — vedi
il controllo.Se modifichi la sonda, non cambiare la lunghezza del
RelayStatedecodificato e non scansionare le lunghezze. Venti byte sono portanti, e più corto non è più sicuro. Altre lunghezze sono non testate nel migliore dei casi e un'interruzione dell'appliance nel peggiore.
Tre richieste HTTP per target, e una quarta solo quando l'appliance restituisce il segnale non patchato:
| # | Richiesta | Scopo |
|---|---|---|
| 1 | GET /logon/LogonPoint/tmindex.html | Identificazione NetScaler (token Content-Security-Policy) |
| 2 | GET /vpn/js/rdx/core/rdx.js | Identificazione NetScaler |
| 3 | POST /cgi/samlauth | la sonda |
| 4 | POST /cgi/samlauth | il controllo, inviato solo dopo un segnale non patchato |
I redirect non vengono seguiti e nessun cookie viene mai rimandato indietro — NetScaler serve un NSC_DLGE scaduto
che un client che lo riproduce porterebbe nella richiesta 4, trasformando il POST in una pagina di logon.
nsppe registra il POST SAML in /var/log/ns.log a livello informativo, senza necessità di alcun interruttore di debug, come
un fallimento di deserializzazione del contesto nFactor che include la riga
nFactor: ctx deserialize aaa_info, base64 decode failed. Non ci sono righe di creazione sessione o login
a questa lunghezza, ed è ciò che distingue una scansione da un tentativo di bypass.
La correzione è una singola istruzione. Su una deserializzazione del contesto fallita la build vulnerabile porta
avanti la lunghezza del RelayState decodificato come stato della richiesta; la build corretta porta la costante
di errore 0xe0005, resa come Malformed Assertion. Quindi un singolo POST le separa:
| Risposta alla sonda | Build | Verdetto |
|---|---|---|
500 Internal Server Error 43524 | non patchata | VULNERABLE |
200 Malformed Assertion sent to Netscaler | patchata | PATCHED |
La build patchata risponde Malformed Assertion a ogni lunghezza, ed è ciò che rende la risposta non patchata
un oracolo piuttosto che una coincidenza.
VULNERABLE si basa su una singola risposta distintiva, quindi lo strumento verifica che la risposta dipenda effettivamente
da ciò che è stato inviato. Dopo il segnale non patchato ripete la richiesta con un RelayState di controllo
della stessa lunghezza decodificata di 20 byte, che differisce solo nei 16 byte dopo ctx=. Quel
contenuto viene rifiutato da una guardia a monte del deserializzatore, quindi ogni build testata — patchata e
non patchata — risponde 200 Malformed Assertion. Il verdetto regge solo se le due risposte differiscono.
Se coincidono, qualcosa sta restituendo 500/43524 indipendentemente dall'input, e il risultato è
INCONCLUSIVE etichettato flat-response anziché un falso VULNERABLE. Mantenere il controllo alla
stessa lunghezza decodificata lo tiene dentro l'involucro di sicurezza validato e isola il contenuto come unica variabile.
La guardia è unidirezionale. Non può validare un verdetto PATCHED, perché un'appliance genuinamente
patchata risponde in modo identico a sonda e controllo — è ciò che significa essere patchati qui. Vedi
Limitazioni.
La deserializzazione del RelayState avviene prima del matching della policy SAML, quindi un virtual server che porta solo una
samlIdPPolicy risponde alla sonda, non solo uno con un samlAction service-provider
associato. Questa è una copertura più ampia di quella che un controllo di precondizione di configurazione può offrire.
La risposta vulnerabile è l'unica auto-corroborante: porta un marcatore specifico, e il
controllo prova che la risposta dipende da ciò che è stato inviato. Anche PATCHED è una prova positiva — l'errore
corretto dell'appliance stessa sul percorso che la sonda ha raggiunto — ma è limitato a questa CVE e a
quel percorso. INCONCLUSIVE ed ERROR non sono affatto prove: la sonda non ha mai raggiunto il
deserializzatore, quindi lo stato della patch è sconosciuto, ed è per questo che INCONCLUSIVE è un verdetto separato
anziché essere accorpato a PATCHED. Riportare il silenzio come un superamento lo sbaglia nella direzione
pericolosa. Dove serve certezza, conferma con show ns version rispetto alle build in
Remediation.
Ogni verdetto porta un breve tag reason. --brief lo stampa come terza colonna e --json
lo porta come reason.
| Verdetto | Tag reason | Significato |
|---|---|---|
VULNERABLE | internal-error-43524 | L'appliance ha propagato la lunghezza del RelayState decodificato come suo errore interno, e un controllo della stessa lunghezza ha provato che la risposta dipende da ciò che è stato inviato. La correzione CTX696939 è assente — applica la patch. |
PATCHED | fixed-error-returned | L'appliance ha restituito l'errore corretto sul percorso che la sonda ha raggiunto. Limitato a questa CVE, e vedi Limitazioni sui dispositivi interposti. |
UNAFFECTED | no-saml-endpoint | /cgi/samlauth ha restituito 404, quindi l'assertion consumer SAML non è servito qui. Per-vserver, non per-appliance, e vedi l'avvertenza sulle build più vecchie in Limitazioni. |
INCONCLUSIVE | flat-response | Sonda e controllo della stessa lunghezza hanno risposto in modo identico, quindi la risposta non dipende da ciò che è stato inviato. La guardia anti-falsi positivi che scatta. |
INCONCLUSIVE | generic-internal-error | L'endpoint ha restituito 43549, il suo errore interno generico, che sia le build patchate che quelle non patchate restituiscono. Non è il discriminatore — e nota che è il marcatore non patchato per la CVE-2026-8452 non correlata, quindi sembra un riscontro e non lo è. |
INCONCLUSIVE | unrecognized-reply | L'endpoint ha risposto con qualcosa al di fuori dell'insieme riconosciuto. |
INCONCLUSIVE | no-probe-response | L'host ha risposto all'identificazione ma non alla sonda — un timeout, un reset, o un middlebox che scarta il POST. Riprova. |
ERROR | not-identified | Non identificato come NetScaler, o irraggiungibile. |
Tutte e quattro le ragioni INCONCLUSIVE significano la stessa cosa per il processo decisionale — sconosciuto, non patchato.
Il tag nomina quale condizione correggere prima di rieseguire.
| Codice | Significato |
|---|---|
0 | Nessun target era VULNERABLE |
1 | Almeno un target è VULNERABLE |
2 | Errore di utilizzo (argomenti errati / file dei target non leggibile) |
L'uscita 0 non è un certificato di buona salute. PATCHED, UNAFFECTED e tutte e quattro le ragioni INCONCLUSIVE
vi confluiscono, e solo una di queste è un risultato positivo. Un wrapper che deve distinguere
"patchato" da "non classificabile" deve leggere il verdetto — prima colonna di --brief, o il
campo verdict di --json — non il codice di uscita.
PATCHED. Malformed Assertion è ciò che
l'appliance restituisce ogni volta che la sonda non riesce a raggiungere il deserializzatore del contesto, e essere patchata è
solo una delle ragioni per cui ciò accade. Un dispositivo interposto che rimuove, altera o cortocircuita il
parametro RelayState produce quindi PATCHED su un'appliance vulnerabile, e nessuna richiesta aggiuntiva che lo strumento potrebbe inviare separerebbe le due. Dove qualcosa può trovarsi davanti
all'appliance, conferma con show ns version.UNAFFECTED è limitato alle build attuali. Su 14.1-43.55 / 13.1-61.27 e precedenti l'
advisory afferma che l'esposizione non richiede affatto una SAML action — qualsiasi virtual server Gateway o AAA
è interessato — quindi un 404 su una tale build non mette l'appliance fuori ambito. Quel ramo
è tratto dall'advisory anziché misurato.PATCHED non dice nulla su qualsiasi altra
vulnerabilità NetScaler, inclusa CVE-2026-19489 nello stesso bollettino.VULNERABLE stabilisce che la correzione
è assente sul percorso che la sonda ha raggiunto. Non misura fin dove un attaccante potrebbe spingere il
bypass sulla tua configurazione, e non ti dirà se qualcuno lo ha già fatto — cerca quello in
/var/log/ns.log separatamente.Aggiorna a 13.1-63.21 o successiva, o 14.1-73.32 o successiva (FIPS e NDcPP: 13.1-37.277 per 13.1-FIPS e 13.1-NDcPP, 14.1-73.32 FIPS per 14.1-FIPS), secondo CTX696939. Le appliance su 12.1 o 13.0 non hanno correzione e non ne riceveranno una — quei rami sono a fine vita e dovrebbero essere migrati a un ramo supportato.
Tre ulteriori note:
defaultAuthorizationAction di alcuna vpn sessionAction associata al vserver — ricade sul
set vpn parameter -defaultAuthorizationAction globale. Il valore predefinito fornito è DENY; ALLOW
è comune sul campo perché è ciò a cui gli amministratori ricorrono invece di scrivere policy di
autorizzazione per utente, e ALLOW è ciò che permette alla sessione non autenticata di raggiungere le risorse
interne. È globale, non per-vserver. Questo limita l'impatto; non è una correzione.add authentication samlAction e add authentication samlIdPProfile insieme a
add authentication vserver e add vpn vserver — fatta salva l'avvertenza sulle build più vecchie sopra.CTX696939 corregge anche CVE-2026-19489, e questa superficie si sovrappone pesantemente a CVE-2026-8452 del bollettino precedente: sulle build da 14.1-43.56 / 13.1-61.28 in poi entrambi i problemi sono vincolati a una SAML action configurata, quindi un singolo audit di configurazione delimita entrambi.
Questo codice è distribuito sotto una licenza MIT.
L'uso di questo strumento per attaccare target senza previo consenso reciproco è illegale. È responsabilità dell'utente finale rispettare tutte le leggi locali, statali e federali applicabili. Gli sviluppatori non si assumono alcuna responsabilità e non sono responsabili per qualsiasi uso improprio o danno causato da questo programma.