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-check — Safely detect Citrix NetScaler SAML auth bypass CVE-2026-19490 | Kitploit
Tools/GitHubGitHub/bishopfox/cve-2026-19490-check
Defensive ToolsVulnerability ScannersWeb Vulnerability ScannersVulnerability AnalysisInformation GatheringWeb SecurityNetwork SecurityPenetration TestingAuthentication
GitHubbishopfox/cve-2026-19490-check

CVE-2026-19490-check

Safely detect Citrix NetScaler SAML auth bypass CVE-2026-19490

116h 34m 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 →
View Repository
Share

Citrix NetScaler SAML nFactor Context Authentication Bypass — Vulnerability Detection Script

A safe, unauthenticated vulnerability check for CVE-2026-19490, the pre-authentication authentication bypass in the Citrix NetScaler ADC / NetScaler Gateway SAML service-provider path (CTX696939, published 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). Reported by Samarth Vashisht of the JPMorgan Chase penetration testing team.

The appliance base64-decodes the RelayState parameter of a SAML response and, when the plaintext begins with ctx=, hands the remainder to the nFactor context deserializer. On an unpatched build a deserialization failure propagates the decoded RelayState length as the request's internal disposition code instead of an error, so an unauthenticated attacker chooses which internal branch the appliance takes next purely by choosing how long the RelayState is. Some branches mint a real Gateway session; others crash the packet engine and restart the appliance. This script does neither — it sends the one length validated to create no session and leave the packet engine alone, and answers a single question per target: A result other than is .

is this appliance vulnerable?
VULNERABLE
not by itself a clean bill of health

Usage

root@kitploit:~
# 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+, standard library only — no third-party packages.

Point the tool at the Gateway or AAA virtual server, not the management interface. Exposure is per virtual server, so an appliance with several VIPs needs each one tested. The probe carries a hard safety envelope — one validated RelayState length, never swept — which Is it Safe to Run? sets out.

Options

FlagDescription
TARGETOne or more [https://]HOST[:PORT] targets; the scheme defaults to https://
-f, --targets-file FILERead targets from a file (one per line; # comments)
--timeout SECSPer-request timeout (default: 15)
--workers NConcurrent targets (default: 16); output stays in input order
-b, --briefSingle aligned line per target — ideal for scanning many hosts
--jsonEmit structured JSON, including every request sent per target
--no-colorDisable coloured output (also honours NO_COLOR and non-TTY)

Examples

A vulnerable appliance (the default two-line output). The [!] marker and VULNERABLE render red on a TTY:

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

A patched appliance:

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

The false-positive guard firing. The probe and the same-length control both returned the unpatched signal, so the reply does not depend on what was sent and the decisive-looking answer is withdrawn:

root@kitploit:~
$ ./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 an estate (--brief). The two gateway.example.com rows are the SP and IdP-only virtual servers on the same appliance — both answer, which a configuration-precondition check would not manage:

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

Machine-readable output (--json). Every request is included, so a finding can be re-derived from the evidence rather than trusted. The control is recorded by its relation to the probe rather than as a verdict of its own, because a control that reads like a patched build is the expected result on every build:

root@kitploit:~
$ ./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"
      }
    ]
  }
]

Is it Safe to Run?

Yes. It is designed for production and assessment use:

  • One fixed length, never a sweep. The probe's RelayState decodes to exactly 20 bytes and the tool sends no other length under any flag. On an unpatched appliance the decoded length is the branch selector, and the branches include ones that create a session and ones that SIGSEGV the packet engine — restarting the whole NetScaler and dropping traffic for roughly 45 seconds. Twenty bytes lands on a clean internal-error branch that creates no session, and the payload builder refuses to construct any other length, so an edit elsewhere cannot quietly widen the probe.
  • Validated by measurement. The 20-byte probe was run 10 times consecutively against an unpatched 13.1-63.18 with zero cores generated and no packet-engine restart. The check itself has since been run against both maintenance branches, on both sides of the fix, and against service-provider and IdP-only virtual servers alike, with no packet-engine restart on any of them.
  • No authentication, no credential material, no state change. No assertion, signature, timestamp, session, or client certificate is presented anywhere in this check. The SAMLResponse parameter must be present for the RelayState branch to be reached, but its content is irrelevant, so it is four bytes of junk. The branch issues no cookie and writes no configuration.
  • False-positive guard. VULNERABLE is never reported on a single response — see the control.

If you modify the probe, do not change the decoded RelayState length and do not sweep lengths. Twenty bytes is load-bearing, and shorter is not safer. Other lengths are untested at best and an appliance outage at worst.

Request and log footprint

Three HTTP requests per target, and a fourth only when the appliance returns the unpatched signal:

#RequestPurpose
1GET /logon/LogonPoint/tmindex.htmlNetScaler identification (Content-Security-Policy tokens)
2GET /vpn/js/rdx/core/rdx.jsNetScaler identification
3POST /cgi/samlauththe probe
4POST /cgi/samlauththe control, sent only after an unpatched signal

Redirects are not followed and no cookie is ever sent back — NetScaler serves an expired NSC_DLGE that a replaying client would carry into request 4, flipping the POST to a logon page.

nsppe logs the SAML POST to /var/log/ns.log at informational level, no debug knob required, as an nFactor context deserialization failure including the line nFactor: ctx deserialize aaa_info, base64 decode failed. There are no session-creation or login lines at this length, which is what distinguishes a scan from an attempt at the bypass.

How it Works

The fix is one instruction. On a failing context deserialization the vulnerable build carries the decoded RelayState length forward as the request's status; the fixed build carries the constant error 0xe0005, rendered as Malformed Assertion. So one POST separates them:

Response to the probeBuildVerdict
500 Internal Server Error 43524unpatchedVULNERABLE
200 Malformed Assertion sent to NetscalerpatchedPATCHED

The patched build answers Malformed Assertion at every length, which is what makes the unpatched answer an oracle rather than a coincidence.

The control is what makes a 500 mean something

VULNERABLE rests on a single distinguishing response, so the tool verifies the response actually depends on what was sent. After the unpatched signal it repeats the request with a control RelayState of the same 20-byte decoded length, differing only in the 16 bytes after ctx=. That content is rejected by a guard ahead of the deserializer, so every build tested — patched and unpatched — answers it 200 Malformed Assertion. The verdict stands only if the two answers differ. If they match, something is returning 500/43524 regardless of input, and the result is INCONCLUSIVE tagged flat-response rather than a false VULNERABLE. Holding the control at the same decoded length keeps it inside the validated safety envelope and isolates content as the only variable.

The guard is one-directional. It cannot validate a PATCHED verdict, because a genuinely patched appliance answers probe and control identically — that is what being patched means here. See Limitations.

It answers on IdP-only virtual servers too

RelayState deserialization runs before SAML policy matching, so a virtual server carrying only a samlIdPPolicy answers the probe, not just one with a service-provider samlAction bound. That is broader coverage than a configuration-precondition check can offer.

A non-match is never a claim of safety

The vulnerable answer is the only self-corroborating one: it carries a specific marker, and the control proves the reply depends on what was sent. PATCHED is positive evidence too — the appliance's own fixed error on the path the probe reached — but it is scoped to this CVE and to that path. INCONCLUSIVE and ERROR are not evidence at all: the probe never reached the deserializer, so patch state is unknown, and that is why INCONCLUSIVE is a separate verdict rather than folded into PATCHED. Reporting silence as a pass gets it wrong in the dangerous direction. Where you need certainty, confirm with show ns version against the builds in Remediation.

Verdicts

Every verdict carries a short reason tag. --brief prints it as the third column and --json carries it as reason.

VerdictReason tagMeaning
VULNERABLEinternal-error-43524The appliance propagated the decoded RelayState length as its internal error, and a same-length control proved the reply depends on what was sent. The CTX696939 fix is absent — patch it.
PATCHEDfixed-error-returnedThe appliance returned the fixed error on the path the probe reached. Scoped to this CVE, and see Limitations on interposed devices.
UNAFFECTEDno-saml-endpoint/cgi/samlauth returned 404, so the SAML assertion consumer is not served here. Per-vserver, not per-appliance, and see the older-build caveat in Limitations.
INCONCLUSIVEflat-responseProbe and same-length control answered identically, so the reply does not depend on what was sent. The false-positive guard firing.
INCONCLUSIVEgeneric-internal-errorThe endpoint returned 43549, its generic internal error, which both patched and unpatched builds return. Not the discriminator — and note it is the unpatched marker for the unrelated CVE-2026-8452, so it looks like a hit and is not one.
INCONCLUSIVEunrecognized-replyThe endpoint answered with something outside the recognized set.
INCONCLUSIVEno-probe-responseThe host answered identification but not the probe — a timeout, a reset, or a middlebox dropping the POST. Retry.
ERRORnot-identifiedNot identified as a NetScaler, or unreachable.

All four INCONCLUSIVE reasons mean the same thing for decision-making — unknown, not patched. The tag names which condition to fix before re-running.

Exit codes

CodeMeaning
0No target was VULNERABLE
1At least one target is VULNERABLE
2Usage error (bad arguments / unreadable targets file)

Exit 0 is not a clean bill of health. PATCHED, UNAFFECTED and all four INCONCLUSIVE reasons collapse into it, and only one of those is a positive result. A wrapper that needs to tell "patched" from "could not be classified" must read the verdict — first column of --brief, or the verdict field of --json — not the exit code.

Limitations

  • A WAF or load balancer can skew a PATCHED verdict. Malformed Assertion is what the appliance returns whenever the probe fails to reach the context deserializer, and being patched is only one reason that happens. An interposed device that strips, mangles, or short-circuits the RelayState parameter therefore produces PATCHED on a vulnerable appliance, and no additional request the tool could send would separate the two. Where something may sit in front of the appliance, confirm with show ns version.
  • UNAFFECTED is scoped to current builds. On 14.1-43.55 / 13.1-61.27 and earlier the advisory states that exposure does not require a SAML action at all — any Gateway or AAA virtual server is affected — so a 404 on such a build does not put the appliance out of scope. That leg is taken from the advisory rather than measured.
  • This checks one CVE, not the appliance's patch level. PATCHED says nothing about any other NetScaler vulnerability, including CVE-2026-19489 in the same bulletin.
  • Not an exploitation check, and not a compromise check. VULNERABLE establishes that the fix is absent on the path the probe reached. It does not measure how far an attacker could take the bypass on your configuration, and it will not tell you whether anyone already has — hunt that in /var/log/ns.log separately.
  • It cannot tell you whether the denial-of-service branches are reachable. The tool sends one validated length and never explores the others, by design.
  • Reachability. A result reflects what the appliance exposes to the network position you run it from.

Remediation

Upgrade to 13.1-63.21 or later, or 14.1-73.32 or later (FIPS and NDcPP: 13.1-37.277 for 13.1-FIPS and 13.1-NDcPP, 14.1-73.32 FIPS for 14.1-FIPS), per CTX696939. Appliances on 12.1 or 13.0 have no fix and will not receive one — those branches are end of life and should be migrated to a supported branch.

Three further notes:

  • Patch both nodes of an HA pair. An unpatched secondary is a fully exposed appliance the moment it takes over.
  • If you cannot patch today, check the one global knob that bounds the impact. The session this bug mints on a Gateway virtual server skips session-policy evaluation, so it does not pick up the defaultAuthorizationAction of any vpn sessionAction bound to the vserver — it falls back to the global set vpn parameter -defaultAuthorizationAction. The shipped default is DENY; ALLOW is common in the field because it is what administrators reach for instead of writing per-user authorization policies, and ALLOW is what lets the unauthenticated session reach internal resources. It is global, not per-vserver. This bounds the impact; it is not a fix.
  • Scope your inventory by SAML configuration, not by virtual server type. Check the running configuration for add authentication samlAction and add authentication samlIdPProfile alongside add authentication vserver and add vpn vserver — subject to the older-build caveat above.

CTX696939 also fixes CVE-2026-19489, and this surface overlaps heavily with CVE-2026-8452 from the previous bulletin: on builds from 14.1-43.56 / 13.1-61.28 onward both issues are gated on a configured SAML action, so one configuration audit scopes both.

License

This code is distributed under an MIT license.

Legal Disclaimer

Usage of this tool for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state, and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program.

See Also

  • Citrix CTX696939 — NetScaler security bulletin
  • NVD — CVE-2026-19490
  • NVD — CVE-2026-19489
  • NVD — CVE-2026-8452
Download Tool