
Citrix NetScaler SAML-Authentifizierungs-Bypass CVE-2026-19490 sicher erkennen
Ein sicherer, unauthentifizierter Schwachstellen-Check für CVE-2026-19490, die Pre-Authentication-
Authentication-Bypass in dem SAML-Service-Provider-Pfad von Citrix NetScaler ADC / NetScaler Gateway
(CTX696939,
veröffentlicht am 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). Gemeldet von Samarth Vashisht vom
Penetrationstest-Team von JPMorgan Chase.
Die Appliance base64-dekodiert den RelayState-Parameter einer SAML-Antwort und übergibt, wenn der
Klartext mit ctx= beginnt, den Rest an den nFactor-Context-Deserializer. Bei einem ungepatchten Build
propagiert ein Deserialisierungsfehler die dekodierte RelayState-Länge als internen
Disposition-Code der Anfrage statt eines Fehlers, sodass ein unauthentifizierter Angreifer allein
dadurch, dass er die Länge des RelayState wählt, bestimmt, welchen internen Zweig die Appliance als
Nächstes nimmt. Einige Zweige erzeugen eine echte Gateway-Session; andere bringen die Packet Engine zum
Absturz und starten die Appliance neu. Dieses Skript tut weder das eine noch das andere — es sendet die
eine validierte Länge, die nachweislich keine Session erzeugt und die Packet Engine unberührt lässt, und
beantwortet pro Ziel eine einzige Frage: Ein anderes Ergebnis als
ist .
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+, nur Standardbibliothek — keine Drittanbieter-Pakete.
Richten Sie das Tool auf den Gateway- oder AAA-Virtual-Server, nicht auf die Management-Schnittstelle.
Die Exposition gilt pro Virtual Server, daher muss bei einer Appliance mit mehreren VIPs jeder einzelne
getestet werden. Die Sonde trägt eine harte Sicherheitshülle — eine validierte RelayState-Länge, niemals
gesweept — die Ist es sicher auszuführen? darlegt.
| Flag | Beschreibung |
|---|---|
TARGET | Ein oder mehrere [https://]HOST[:PORT]-Ziele; das Schema ist standardmäßig https:// |
-f, --targets-file FILE | Ziele aus einer Datei lesen (eine pro Zeile; #-Kommentare) |
--timeout SECS | Timeout pro Anfrage (Standard: 15) |
--workers N | Gleichzeitige Ziele (Standard: 16); die Ausgabe bleibt in Eingabereihenfolge |
-b, --brief | Eine ausgerichtete Zeile pro Ziel — ideal zum Scannen vieler Hosts |
--json | Strukturiertes JSON ausgeben, einschließlich jeder pro Ziel gesendeten Anfrage |
--no-color | Farbige Ausgabe deaktivieren (berücksichtigt auch NO_COLOR und Nicht-TTY) |
Eine verwundbare Appliance (die standardmäßige zweizeilige Ausgabe). Der [!]-Marker und VULNERABLE
werden auf einem TTY rot dargestellt:
$ ./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
Eine gepatchte Appliance:
$ ./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)
Der False-Positive-Guard greift. Die Sonde und die gleich lange Kontrolle lieferten beide das ungepatchte Signal zurück, sodass die Antwort nicht davon abhängt, was gesendet wurde, und die entscheidend wirkende Antwort zurückgezogen wird:
$ ./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
Sweeping eines Bestands (--brief). Die beiden gateway.example.com-Zeilen sind die SP- und die
IdP-only-Virtual-Server auf derselben Appliance — beide antworten, was ein
Konfigurations-Vorbedingungs-Check nicht schaffen würde:
$ ./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
Maschinenlesbare Ausgabe (--json). Jede Anfrage ist enthalten, sodass ein Befund aus den Belegen
rekonstruiert werden kann, statt ihm zu vertrauen. Die Kontrolle wird über ihre Beziehung zur Sonde
erfasst und nicht als eigenes Urteil, weil eine Kontrolle, die wie ein gepatchter Build aussieht, auf
jedem Build das erwartete Ergebnis ist:
$ ./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"
}
]
}
]
Ja. Es ist für den Produktiv- und Assessment-Einsatz konzipiert:
RelayState der Sonde dekodiert zu genau 20 Bytes, und
das Tool sendet unter keinem Flag eine andere Länge. Bei einer ungepatchten Appliance ist die
dekodierte Länge der Zweigselektor, und die Zweige umfassen solche, die eine Session erzeugen, und
solche, die die Packet Engine per SIGSEGV zum Absturz bringen — was den gesamten NetScaler neu startet
und den Datenverkehr für etwa 45 Sekunden unterbricht. Zwanzig Bytes landen auf einem sauberen
Internal-Error-Zweig, der keine Session erzeugt, und der Payload-Builder weigert sich, eine andere
Länge zu konstruieren, sodass eine Änderung an anderer Stelle die Sonde nicht stillschweigend
erweitern kann.13.1-63.18 ausgeführt, wobei null Cores erzeugt und kein Packet-Engine-Neustart ausgelöst wurde. Der
Check selbst wurde seither gegen beide Maintenance-Branches, auf beiden Seiten des Fixes und sowohl
gegen Service-Provider- als auch gegen IdP-only-Virtual-Server ausgeführt, ohne Packet-Engine-Neustart
bei irgendeinem davon.SAMLResponse-Parameter muss vorhanden sein, damit der RelayState-Zweig erreicht
wird, aber sein Inhalt ist irrelevant, daher sind es vier Bytes Müll. Der Zweig setzt kein Cookie und
schreibt keine Konfiguration.VULNERABLE wird niemals aufgrund einer einzelnen Antwort gemeldet — siehe
die Kontrolle.Wenn Sie die Sonde ändern, ändern Sie nicht die dekodierte
RelayState-Länge und sweepen Sie keine Längen. Zwanzig Bytes sind tragend, und kürzer ist nicht sicherer. Andere Längen sind bestenfalls ungetestet und schlimmstenfalls ein Ausfall der Appliance.
Drei HTTP-Anfragen pro Ziel und eine vierte nur dann, wenn die Appliance das ungepatchte Signal zurückgibt:
| # | Anfrage | Zweck |
|---|---|---|
| 1 | GET /logon/LogonPoint/tmindex.html | NetScaler-Identifikation (Content-Security-Policy-Token) |
| 2 | GET /vpn/js/rdx/core/rdx.js | NetScaler-Identifikation |
| 3 | POST /cgi/samlauth | die Sonde |
| 4 | POST /cgi/samlauth | die Kontrolle, nur nach einem ungepatchten Signal gesendet |
Weiterleitungen werden nicht verfolgt und es wird niemals ein Cookie zurückgesendet — NetScaler liefert
ein abgelaufenes NSC_DLGE, das ein wiederspielender Client in Anfrage 4 tragen würde, wodurch der POST
auf eine Logon-Seite umgeleitet würde.
nsppe protokolliert den SAML-POST in /var/log/ns.log auf Informationslevel, ohne dass ein
Debug-Schalter erforderlich ist, als nFactor-Context-Deserialisierungsfehler einschließlich der Zeile
nFactor: ctx deserialize aaa_info, base64 decode failed. Bei dieser Länge gibt es keine
Session-Erzeugungs- oder Login-Zeilen, was einen Scan von einem Versuch des Bypasses unterscheidet.
Der Fix ist eine einzige Instruktion. Bei einer fehlschlagenden Context-Deserialisierung trägt der
verwundbare Build die dekodierte RelayState-Länge als Status der Anfrage weiter; der gefixte Build
trägt den konstanten Fehler 0xe0005 weiter, dargestellt als Malformed Assertion. Ein POST trennt sie
also:
| Antwort auf die Sonde | Build | Urteil |
|---|---|---|
500 Internal Server Error 43524 | ungepatcht | VULNERABLE |
200 Malformed Assertion sent to Netscaler | gepatcht | PATCHED |
Der gepatchte Build antwortet bei jeder Länge mit Malformed Assertion, was die ungepatchte Antwort
zu einem Orakel statt zu einem Zufall macht.
VULNERABLE stützt sich auf eine einzige unterscheidende Antwort, daher verifiziert das Tool, dass die
Antwort tatsächlich davon abhängt, was gesendet wurde. Nach dem ungepatchten Signal wiederholt es die
Anfrage mit einer Kontroll-RelayState von derselben dekodierten Länge von 20 Bytes, die sich nur in
den 16 Bytes nach ctx= unterscheidet. Dieser Inhalt wird von einem Guard vor dem Deserializer
zurückgewiesen, sodass jeder getestete Build — gepatcht und ungepatcht — darauf mit
200 Malformed Assertion antwortet. Das Urteil bleibt nur bestehen, wenn die beiden Antworten
voneinander abweichen. Wenn sie übereinstimmen, gibt etwas unabhängig von der Eingabe 500/43524
zurück, und das Ergebnis ist INCONCLUSIVE mit dem Tag flat-response statt eines falschen
VULNERABLE. Die Kontrolle auf derselben dekodierten Länge zu halten, bewahrt sie innerhalb der
validierten Sicherheitshülle und isoliert den Inhalt als einzige Variable.
Der Guard ist einseitig. Er kann kein PATCHED-Urteil validieren, weil eine tatsächlich gepatchte
Appliance auf Sonde und Kontrolle identisch antwortet — genau das bedeutet hier gepatcht zu sein. Siehe
Einschränkungen.
Die RelayState-Deserialisierung läuft vor dem SAML-Policy-Matching, sodass ein Virtual Server, der
nur eine samlIdPPolicy trägt, auf die Sonde antwortet, nicht nur einer mit einer gebundenen
Service-Provider-samlAction. Das ist eine breitere Abdeckung, als ein
Konfigurations-Vorbedingungs-Check bieten kann.
Die verwundbare Antwort ist die einzige, die sich selbst bestätigt: Sie trägt einen spezifischen Marker,
und die Kontrolle beweist, dass die Antwort davon abhängt, was gesendet wurde. PATCHED ist ebenfalls
ein positiver Beleg — der eigene gefixte Fehler der Appliance auf dem Pfad, den die Sonde erreicht hat —
aber er ist auf diese CVE und auf diesen Pfad beschränkt. INCONCLUSIVE und ERROR sind überhaupt
keine Belege: Die Sonde hat den Deserializer nie erreicht, der Patch-Status ist also unbekannt, und
deshalb ist INCONCLUSIVE ein separates Urteil statt in PATCHED eingefaltet. Stille als Bestehen zu
melden, liegt in der gefährlichen Richtung falsch. Wo Sie Gewissheit benötigen, bestätigen Sie mit
show ns version gegen die Builds in Behebung.
Jedes Urteil trägt ein kurzes reason-Tag. --brief gibt es als dritte Spalte aus und --json trägt es
als reason.
| Urteil | Reason-Tag | Bedeutung |
|---|---|---|
VULNERABLE | internal-error-43524 | Die Appliance propagierte die dekodierte RelayState-Länge als ihren internen Fehler, und eine gleich lange Kontrolle bewies, dass die Antwort davon abhängt, was gesendet wurde. Der CTX696939-Fix fehlt — patchen Sie. |
PATCHED | fixed-error-returned | Die Appliance gab den gefixten Fehler auf dem Pfad zurück, den die Sonde erreicht hat. Beschränkt auf diese CVE, und siehe Einschränkungen zu zwischengeschalteten Geräten. |
UNAFFECTED | no-saml-endpoint | /cgi/samlauth gab 404 zurück, der SAML-Assertion-Consumer wird hier also nicht bedient. Pro Vserver, nicht pro Appliance, und siehe den Vorbehalt zu älteren Builds in Einschränkungen. |
INCONCLUSIVE | flat-response | Sonde und gleich lange Kontrolle antworteten identisch, die Antwort hängt also nicht davon ab, was gesendet wurde. Der False-Positive-Guard greift. |
INCONCLUSIVE | generic-internal-error | Der Endpunkt gab 43549 zurück, seinen generischen internen Fehler, den sowohl gepatchte als auch ungepatchte Builds zurückgeben. Nicht der Diskriminator — und beachten Sie, dass er der ungepatchte Marker für die unabhängige CVE-2026-8452 ist, sodass er wie ein Treffer aussieht und keiner ist. |
INCONCLUSIVE | unrecognized-reply | Der Endpunkt antwortete mit etwas außerhalb der erkannten Menge. |
INCONCLUSIVE | no-probe-response | Der Host antwortete auf die Identifikation, aber nicht auf die Sonde — ein Timeout, ein Reset oder eine Middlebox, die den POST verwirft. Erneut versuchen. |
ERROR | not-identified | Nicht als NetScaler identifiziert oder nicht erreichbar. |
Alle vier INCONCLUSIVE-Gründe bedeuten für die Entscheidungsfindung dasselbe — unbekannt, nicht
gepatcht. Das Tag benennt, welche Bedingung vor dem erneuten Ausführen zu beheben ist.
| Code | Bedeutung |
|---|---|
0 | Kein Ziel war VULNERABLE |
1 | Mindestens ein Ziel ist VULNERABLE |
2 | Verwendungsfehler (ungültige Argumente / nicht lesbare Zieldatei) |
Exit 0 ist keine Unbedenklichkeitsbescheinigung. PATCHED, UNAFFECTED und alle vier
INCONCLUSIVE-Gründe fallen darunter zusammen, und nur eines davon ist ein positives Ergebnis. Ein
Wrapper, der „gepatcht" von „konnte nicht klassifiziert werden" unterscheiden muss, muss das Urteil
lesen — erste Spalte von --brief oder das verdict-Feld von --json — nicht den Exit-Code.
PATCHED-Urteil verzerren. Malformed Assertion ist
das, was die Appliance zurückgibt, wann immer die Sonde den Context-Deserializer nicht erreicht, und
gepatcht zu sein ist nur einer der Gründe dafür. Ein zwischengeschaltetes Gerät, das den
RelayState-Parameter entfernt, verunstaltet oder kurzschließt, erzeugt daher PATCHED auf einer
verwundbaren Appliance, und keine zusätzliche Anfrage, die das Tool senden könnte, würde die beiden
trennen. Wo etwas vor der Appliance sitzen könnte, bestätigen Sie mit show ns version.UNAFFECTED ist auf aktuelle Builds beschränkt. Bei 14.1-43.55 / 13.1-61.27 und früher
besagt das Advisory, dass die Exposition überhaupt keine SAML-Action erfordert — jeder Gateway- oder
AAA-Virtual-Server ist betroffen —, sodass ein 404 auf einem solchen Build die Appliance nicht aus
dem Geltungsbereich nimmt. Dieser Teil ist dem Advisory entnommen und nicht gemessen.PATCHED sagt nichts über irgendeine
andere NetScaler-Schwachstelle aus, einschließlich CVE-2026-19489 im selben Bulletin.VULNERABLE belegt, dass der Fix auf
dem Pfad fehlt, den die Sonde erreicht hat. Es misst nicht, wie weit ein Angreifer den Bypass bei
Ihrer Konfiguration treiben könnte, und es sagt Ihnen nicht, ob jemand dies bereits getan hat — das
suchen Sie separat in /var/log/ns.log.Aktualisieren Sie auf 13.1-63.21 oder später bzw. 14.1-73.32 oder später (FIPS und NDcPP: 13.1-37.277 für 13.1-FIPS und 13.1-NDcPP, 14.1-73.32 FIPS für 14.1-FIPS), gemäß CTX696939. Appliances auf 12.1 oder 13.0 haben keinen Fix und werden keinen erhalten — diese Branches sind End of Life und sollten auf einen unterstützten Branch migriert werden.
Drei weitere Hinweise:
defaultAuthorizationAction irgendeiner an den
Vserver gebundenen vpn sessionAction aufgreift — sie fällt auf die globale
set vpn parameter -defaultAuthorizationAction zurück. Der ausgelieferte Standard ist DENY;
ALLOW ist in der Praxis verbreitet, weil Administratoren danach greifen, statt
benutzerspezifische Autorisierungsrichtlinien zu schreiben, und ALLOW ist es, was die
unauthentifizierte Session auf interne Ressourcen zugreifen lässt. Sie ist global, nicht pro
Vserver. Dies begrenzt die Auswirkung; es ist kein Fix.add authentication samlAction und add authentication samlIdPProfile
neben add authentication vserver und add vpn vserver — vorbehaltlich des obigen Vorbehalts zu
älteren Builds.CTX696939 behebt auch CVE-2026-19489, und diese Angriffsfläche überschneidet sich stark mit CVE-2026-8452 aus dem vorherigen Bulletin: Bei Builds ab 14.1-43.56 / 13.1-61.28 sind beide Probleme an eine konfigurierte SAML-Action geknüpft, sodass ein Konfigurationsaudit beide abgrenzt.
Dieser Code wird unter einer MIT-Lizenz vertrieben.
Die Verwendung dieses Tools zum Angriff auf Ziele ohne vorherige gegenseitige Zustimmung ist illegal. Es liegt in der Verantwortung des Endnutzers, alle geltenden lokalen, staatlichen und bundesstaatlichen Gesetze zu befolgen. Die Entwickler übernehmen keine Haftung und sind nicht verantwortlich für Missbrauch oder Schäden, die durch dieses Programm verursacht werden.