Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-19490 — NetScaler ADC/Gateway SAML unsigned-assertion bypass via HTTP-Redirect binding (CTX696939) - root cause analysis + PoC | Kitploit
Tools/GitHubGitHub/tarpeg007/cve-2026-19490
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingBinary AnalysisAuthenticationRed Teaming
GitHubtarpeg007/cve-2026-19490

CVE-2026-19490

NetScaler ADC/Gateway SAML unsigned-assertion bypass via HTTP-Redirect binding (CTX696939) - root cause analysis + PoC

View Repository
19h 4m agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-19490 — NetScaler ADC/Gateway SAML authentication bypass

Unauthenticated session forgery on Citrix NetScaler ADC / NetScaler Gateway via the SAML HTTP-Redirect binding handler at GET /cgi/samlauth. CVSS 4.0 9.3, CWE-288. Bulletin CTX696939 (2026-08-19), no workarounds. Credit for the original report goes to Samarth Vashisht (JPMorgan Chase pen-test team); the root-cause analysis and the code in this repo are my own work.

Affected: 14.1 before 14.1-73.32, 13.1 before 13.1-63.21. Fixed in those two builds.

root cause

Two things go wrong together in nsppe, the packet engine.

1. the redirect binding parses assertions with the strict flag cleared.

All call sites of the SAML response parser (sub_b40a50) set up a "strict" argument before the call. The POST binding path (what browsers actually use for SAML responses) passes it set. The HTTP-Redirect binding path does not:

root@kitploit:~
$ 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            <-- the parser

That is the alternate path in the CWE-288 sense. Same request surface, weaker parser invocation, reachable by anyone who can send a GET with a SAMLResponse query parameter.

2. the unsigned-assertion gate treats the default config as ALLOW.

Inside the redirect handler, when the request carries no SigAlg/Signature, the config word for rejectUnsignedAssertion is compared and branched like this:

root@kitploit:~
$ 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            <-- jumps to the ACCEPT path

The word values are: 2 = rejectUnsignedAssertion ON (the default), 3 = STRICT. The je sends 2 to accept. Only STRICT reaches the deny log line:

root@kitploit:~
$ 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

So on a default-config box, an unsigned assertion handed to the redirect binding is parsed (strict off), accepted past the unsigned gate (ON misread as allow), and then runs the ordinary post-parse steps: issuer/audience/subject checks against the SAML action config, then session construction from attacker-supplied fields. No digest, no RSA verify, anywhere on that route. POST binding is not affected the same way — it passes strict to the parser and rejects unsigned input properly.

Preconditions per the bulletin, confirmed against the binary: builds from 14.1-43.56 / 13.1-61.28 onward need a SAML action bound to a Gateway or AAA vserver (the normal SAML SSO setup, so most SAML deployments qualify). Earlier builds register the route with the vserver alone.

exploit shape

One GET. Build a SAML Response with no <ds:Signature> anywhere, DEFLATE + base64 it, and send:

root@kitploit:~
GET /cgi/samlauth?SAMLResponse=<b64(raw-deflate(xml))>&RelayState=<ctx> HTTP/1.1
Host: <gateway>

The values that must match the target's SAML action config: assertion Issuer = the IdP entity ID, Audience = the SP entity ID, Recipient/Destination = the ACS URL, and on transactional setups an InResponseTo from a live AuthnRequest. --mint walks the gateway's own pre-auth login redirect to capture those (the SAMLRequest in the Location header carries all of them). A 302 to /vpn/ plus a real NSC_AAAC / NSC_TASS cookie (not the xyz deletion markers) is a forged session as whatever NameID you put in.

usage

root@kitploit:~
pip install requests

# is the endpoint there and does GET binding process SAMLResponse at all
python3 poc.py https://vpn.target.com --check-only

# non-intrusive config probe: unsigned assertion with a deliberately WRONG issuer.
#   'Malformed Assertion' (0xe0005)  -> STRICT, not vulnerable to this vector
#   issuer/policy error (0xe0012)    -> default config, vulnerable; no session minted
python3 poc.py https://vpn.target.com --safe-oracle

# full chain (authorized targets only): mint the SP chain, forge, validate once
python3 poc.py https://vpn.target.com --mint --name-id [email protected]

--safe-oracle exists because the two configs return different error pages before anything session-shaped happens, which is also how defenders can self-check without touching a real IdP. Run it against your own gear.

demo

demo/demo.gif (also demo.mp4, and demo/demo.cast if you want to play it with asciinema play): affected build out of the docker image, the word-2 default config, the two binary branches disassembled from the shipped nsppe, and the PoC endpoint check. The last mile, session issuance, needs a licensed VPX — CPX Express refuses AAA sessions at the licence layer — which is what lab/record-demo.sh captures when you have one.

lab

lab/setup-cpx.sh brings up the exact affected build in docker:

root@kitploit:~
docker run -dt --privileged --name cpx19490 -e EULA=YES \
    quay.io/netscaler/netscaler-cpx:14.1-73.30
bash lab/setup-cpx.sh

and configures a SAML action with rejectUnsignedAssertion ON, a policy, and a Gateway vserver. Two caveats learned the hard way:

  • CPX Express carries no SSLVPN/AAA user license. The vserver serves /cgi/samlauth but every request lands on 480 Login exceeds maximum allowed users. Good enough to reproduce config + endpoint + binary state, not the final session cookie.
  • For the full session-issuance run you want a VPX with the free Developer Edition license (My Citrix → downloads → NetScaler VPX, then CTX587663 for the license flow). Same CLI as in the setup script, then lab/record-demo.sh records the whole asciinema sequence: version, config, safe-oracle, forged session, STRICT negative control.

The shipped-binary offsets above come straight out of that image:

root@kitploit:~
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

detection / mitigation

  • Upgrade to 14.1-73.32+ / 13.1-63.21+. There is no supported workaround.
  • set samlAction <name> -samlRejectUnsignedAssertion STRICT blocks the redirect vector on the vulnerable path (it makes word==3). Be aware STRICT also changes what the box expects from your IdP (Response + Assertion signing requirements), which is presumably why Citrix ships ON as the default and why "just set STRICT" is not a clean workaround for everyone.
  • Detect: requests to /cgi/samlauth carrying SAMLResponse on GET (redirect binding responses are rare in the wild — browsers POST), unsigned payloads, and the error-page differential above.

legal

For authorized security testing only: your own lab, or targets explicitly in scope for a program you are authorized on. The author is not affiliated with Citrix or the original reporting team.

timeline

  • 2026-08-19 — Citrix bulletin CTX696939, fixes shipped
  • 2026-09 — this root-cause writeup and PoC

MIT license, see LICENSE.

Download Tool