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-8452-check — Behavioral patch-state detector for Citrix NetScaler CVE-2026-8452. Sends crafted SAML requests to determine whether the PrefixList size check is present, without exploiting or corrupting memory. | Kitploit
Tools/GitHubGitHub/bishopfox/cve-2026-8452-check
Vulnerability ScannersVulnerability AnalysisWeb SecurityNetwork Security
GitHubbishopfox/cve-2026-8452-check

CVE-2026-8452-check

Behavioral patch-state detector for Citrix NetScaler CVE-2026-8452. Sends crafted SAML requests to determine whether the PrefixList size check is present, without exploiting or corrupting memory.

View Repository
14 days 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

Citrix NetScaler SAML PrefixList Heap Overflow — Patch-State Detection Script

A safe, non-destructive patch-state check for CVE-2026-8452, the pre-authentication heap overflow in the Citrix NetScaler ADC / NetScaler Gateway SAML signature canonicalizer (CTX696604, CVSS 8.8). An oversized exclusive-canonicalization PrefixList overflows a fixed-size buffer during canonicalization, which NetScaler performs before validating the signature that carries it — so the whole path is reachable with no credentials, no session, and no valid signature. Reported by Michael Tucker of the JPMorgan Chase XOR team; root-cause and exploitation analysis credit to watchTowr Labs.

This script does not exploit the bug and does not corrupt memory. It answers one question per target: is the fix present on this appliance? — determined behaviourally, by observing the patch rather than guessing the build.

Is it Safe to Run?

Yes. It is designed for production and assessment use:

  • The probe stays below the corruption threshold. 575 bytes is long enough that patched and unpatched builds answer differently, and well below the length at which an unpatched appliance's memory corruption begins. Validated by measurement on appliances spanning both supported branches and both patch states, including both fix builds.
  • The threshold it clears is a fixed code constant, not a property of one deployment. Fixed builds accept a PrefixList of 512 bytes and reject 513 or more. That limit was located to the byte, is identical on both supported branches, and does not move with the appliance's configuration or with the shape of the surrounding SAML message — confirmed by probing both routes, which wrap the value in substantially different amounts of XML, and finding they change behaviour at the same byte. 575 clears the limit by 63 bytes, so the verdict does not depend on how a target happens to be set up.
Download Tool
  • Patching does not make appliances reject long SAML values generally. The limit applies to the PrefixList attribute specifically. Inflating other fields past it — assertion consumer service URLs, issuer names, algorithm identifiers, digest and signature values — changes nothing on a fixed build, so applying the fix should not cause a working SAML configuration to start failing.
  • No memory is corrupted and no process is restarted. On a patched build the probe is rejected at the size check; on an unpatched build it fails benignly inside the parser. Neither reaches the overflow.
  • Nothing sensitive enters scan output. The probe carries only synthetic namespace-prefix tokens, and the tool reports a verdict, not response bodies.
  • Two fixed lengths, never a sweep. The 575-byte probe, plus a 35-byte control on the route that answers. The tool never sweeps a range of lengths and never sends any other length.
  • If you modify the probe, do not change PROBE_PREFIXES and do not sweep lengths. 575 bytes is load-bearing. Other PrefixList lengths can destabilize an appliance, in at least one case on a build that carries this fix, so a length sweep is not a safe way to explore this bug and shorter is not safer.

    How it Works

    Patched builds reject an oversized PrefixList cleanly, with a distinctive message. Unpatched builds fall through the parser and return a generic internal error. One identical request, two different answers:

    575-byte PrefixListResponse
    Unpatched500 Internal Server Error 43549
    Patched200 Malformed Assertion sent to Netscaler

    Two routes are tried, IdP first, stopping as soon as one gives an answer. Either is sufficient alone, and together they cover both SAML roles:

    RouteRequestRequires
    1 (first)POST /saml/login — signed AuthnRequest, PrefixList in ds:SignedInfoa SAML IdP policy bound to the targeted vserver
    2 (fallback)POST /cgi/samlauth — SAMLResponse, PrefixList in the assertion signaturea SAML SP assertion consumer service on the targeted vserver

    The IdP route goes first because it is the more robust of the two. It is insensitive to the Issuer value, to the AssertionConsumerServiceURL, and to clock skew — an IssueInstant well outside the appliance's skew tolerance still discriminates correctly, because canonicalization precedes the time check as well as the signature check.

    The route-1 AuthnRequest must be signed. An unsigned one returns 200 Malformed Assertion sent to Netscaler on patched and unpatched builds, which is byte-identical to the patched signal, so a probe that omits the signature block reports every appliance as patched. The signature does not need to be valid, and this tool's is not; it only has to be present, because its SignedInfo is what carries the PrefixList into the canonicalizer.

    Behaviour at the fix boundary

    Both supported branches change behaviour exactly at their fix build, on both routes:

    BuildVerdict
    13.1-63.16last vulnerable 13.1VULNERABLE
    13.1-63.18first fixed 13.1PATCHED
    14.1-66.59vulnerable 14.1VULNERABLE
    14.1-72.61first fixed 14.1PATCHED

    13.1-63.16 and 63.18 are consecutive releases, so the change is attributable to the patch itself rather than to drift across intervening builds.

    Those are the builds where this fix first appeared, and the probe detects exactly that transition. They are no longer the builds to upgrade to: later bulletins have superseded them, so 13.1-63.18 and 14.1-72.61 both answer PATCHED here while remaining exposed to newer issues. See Remediation for the current fixed builds.

    Why not fingerprint the build?

    Because it cannot work on this bug, even in principle. 13.1-63.16 and 13.1-63.18, the builds immediately either side of the fix, serve byte-identical tmindex.html, base.css and resources.js — the fix touches no web asset. Static asset hashes also collide across branches, so a hash-based approach can resolve a vulnerable appliance to a patched build and report it as clean, which is the worst failure mode a detection tool has. Build fingerprinting is therefore deliberately not implemented. Patch state comes from the probe, or from show ns version where you have credentials.

    Requirements

    • Python 3.8+, standard library only — no third-party packages.

    Usage

    root@kitploit:~
    # single target
    ./cve_2026_8452_check.py https://gateway.example.com
    
    # a specific AAA / Gateway virtual server
    ./cve_2026_8452_check.py https://gateway.example.com:9443
    
    # scan a list, one target per line ('#' comments allowed), compact output
    ./cve_2026_8452_check.py -f targets.txt --brief
    
    # machine-readable output for pipelines
    ./cve_2026_8452_check.py -f targets.txt --json > results.json
    

    Point the tool at the Gateway or AAA virtual server, not the management interface. The precondition is per virtual server, so an appliance with several VIPs needs each one tested.

    Options

    FlagDescription
    URLOne or more https://HOST[:PORT] targets
    -f, --targets-file FILERead targets from a file (one per line; # comments)
    -b, --briefSingle aligned line per target — verdict, target, reason tag — for scanning many hosts
    --jsonEmit structured JSON results
    --no-colorDisable coloured output (also honours NO_COLOR and non-TTY)
    --timeout SECSPer-request timeout (default: 15)

    Examples

    An unpatched appliance, answered on the IdP route and confirmed against the control:

    root@kitploit:~
    $ ./cve_2026_8452_check.py https://gateway.example.com:9443
    ====================================================================
      CVE-2026-8452 - NetScaler SAML PrefixList patch-state check
      https://gateway.example.com:9443
    ====================================================================
    
    >> Identifying the appliance
         [ OK ]  NetScaler indicators: 5 (CSP contains citrixng://)
    >> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
         idp /saml/login      HTTP 500 / 43549: no size check present
         idp /saml/login      35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check
         [FAIL]  Size check absent (via the IDP route)
    
    ====================================================================
                             RESULT: VULNERABLE
    ====================================================================
    
      https://gateway.example.com:9443 via IDP  [size-check-absent]
    
      The size check is absent. This appliance is unpatched for
      CVE-2026-8452. Upgrade to 13.1-63.21+ / 14.1-73.32+ (12.1 and
      13.0 are EOL and never fixed).
    
    ====================================================================
    

    The control line is worth reading closely: the 35-byte request clears the size check and is then rejected for its stale IssueInstant, while the 575-byte probe never got that far. That is the ordering the whole method rests on — canonicalization runs before the time check, just as it runs before the signature check.

    A patched appliance, the same request against the fix build. Only the probe line differs — the oversized PrefixList is rejected by name instead of falling through to the internal error:

    root@kitploit:~
    >> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
         idp /saml/login      HTTP 200 "Malformed Assertion": size check rejected the probe
         idp /saml/login      35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check
         [ OK ]  Size check present (via the IDP route)
    
                              RESULT: PATCHED
    
      https://vpn.example.com via IDP  [size-check-present]
    

    Falling back to the SP route. Here the IdP endpoint is reachable but no IdP policy is bound to this virtual server, so route 1 abstains and route 2 answers. When neither route matches a policy the verdict is INCONCLUSIVE tagged no-policy-match — never PATCHED, which is the whole reason that verdict exists:

    root@kitploit:~
    >> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
         idp /saml/login      HTTP 200 "Matching policy not found": parser not reached
         sp  /cgi/samlauth    HTTP 500 / 43549: no size check present
         sp  /cgi/samlauth    35-byte control: HTTP 200 "assertion rejected before the size check": a different SAML condition, not the size check
         [FAIL]  Size check absent (via the SP route)
    
                             RESULT: VULNERABLE
    

    The control caught it. Here the endpoint answered with the patched message at both lengths, so the size check was never exercised and the decisive-looking answer is withdrawn. This is the false-positive guard firing, and the reason it fired is stated rather than left to be inferred:

    root@kitploit:~
    >> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
         idp /saml/login      HTTP 200 "Malformed Assertion": size check rejected the probe
         idp /saml/login      35-byte control: HTTP 200 "Malformed Assertion": size check rejected the probe
         [WARN]  Probe and control answered alike, so the size check was never exercised
    
                            RESULT: INCONCLUSIVE
    
      https://sp-strict.example.com via IDP  [flat-response]
    
      The 575-byte probe and the 35-byte control got the same answer,
      so this endpoint replies the same way whatever it is sent and
      the size check was never exercised. Unknown, not patched.
    

    Scanning a fleet (--brief), one aligned line per target ending in the reason tag. Exit status is 1 if any target is VULNERABLE:

    root@kitploit:~
    $ ./cve_2026_8452_check.py -f targets.txt --brief; echo "exit: $?"
    VULNERABLE       https://gateway.example.com:9443   size-check-absent
    VULNERABLE       https://gateway.example.com:9444   size-check-absent
    PATCHED          https://vpn.example.com            size-check-present
    INCONCLUSIVE     https://sp-strict.example.com      flat-response
    INCONCLUSIVE     https://gw-nopolicy.example.com    no-policy-match
    UNAFFECTED       https://mgmt.example.com           no-saml-endpoint
    ERROR            https://offline.example.com        not-identified
    exit: 1
    

    Machine-readable output (--json), which records every route attempted. verdict, reason and detail are the authoritative fields; attempts is the raw evidence, so an individual attempt may read patched on a target whose verdict is INCONCLUSIVE:

    root@kitploit:~
    $ ./cve_2026_8452_check.py https://vpn.example.com --json
    [
      {
        "target": "https://vpn.example.com",
        "verdict": "PATCHED",
        "reason": "size-check-present",
        "route": "idp",
        "detail": "HTTP 200 \"Malformed Assertion\": size check rejected the probe",
        "attempts": [
          {
            "route": "idp",
            "path": "/saml/login",
            "state": "patched",
            "detail": "HTTP 200 \"Malformed Assertion\": size check rejected the probe"
          },
          {
            "route": "idp",
            "path": "/saml/login",
            "state": "control:known-error",
            "detail": "35-byte control: HTTP 200 \"message timestamp outside the appliance's skew tolerance\": a different SAML condition, not the size check"
          }
        ],
        "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)"
        ]
      }
    ]
    

    Verdicts

    Every verdict carries a short reason tag naming the condition behind it. --brief prints the tag as its third column, and --json carries it as reason.

    VerdictReason tagMeaning
    VULNERABLEsize-check-absentThe size check is absent. This appliance is unpatched — patch it.
    PATCHEDsize-check-presentThe size check fired on the code path the probe reached. Scoped to this CVE: it does not mean the appliance is on a current build.
    UNAFFECTEDno-saml-endpointNo SAML endpoint answered on this virtual server, so the vulnerable path is not reachable here. Per-vserver, not per-appliance: SAML may be configured on another vserver or VIP on the same box.
    INCONCLUSIVEflat-responseThe endpoint answered the 575-byte probe and the 35-byte control identically, so the size check was never exercised. The decisive-looking answer is withdrawn — this is the false-positive guard firing.
    INCONCLUSIVEno-policy-matchA SAML endpoint answered but no bound policy matched the probe, so neither route reached the canonicalizer.
    INCONCLUSIVEother-saml-errorA recognized but non-diagnostic SAML condition rejected the probe before the size check — a different length limit, a signature policy, a timestamp.
    INCONCLUSIVEunrecognized-replyA SAML surface answered with something outside the recognized set.
    ERRORnot-identifiedNot identified as a NetScaler, or unreachable.

    All four INCONCLUSIVE reasons mean the same thing for decision-making — unknown, not patched. Confirm with show ns version. The tag exists to tell an operator which condition to fix before re-running: point the probe at a different virtual server, or bind a matching policy.

    INCONCLUSIVE exists as a distinct verdict, with its own exit code, because a vulnerable appliance can decline to answer the probe. If the SAML policy bound to a virtual server does not match the probe's request, the appliance short-circuits ahead of the canonicalizer and returns nothing diagnostic. A scanner that merely fails to match goes silent on such a host, and silence reads as "patched". This tool reports it as unknown instead.

    Every verdict is confirmed against a control

    PATCHED and VULNERABLE both rest on a single distinguishing response, so the tool verifies that the response actually depends on what was sent. After a decisive answer it repeats the request with a short 35-byte PrefixList — below any size check — and the verdict stands only if the two answers differ. If they match, the endpoint replies the same way whatever it receives, the size check was never exercised, and the result is INCONCLUSIVE.

    This is not hypothetical. A service provider configured with samlRejectUnsignedAssertion STRICT rejects the probe for a missing signature before canonicalization, and answers with the patched message at every length. Without the control, such an appliance reports PATCHED with exit 0 — observed on a genuinely vulnerable build. It now reports INCONCLUSIVE with the reason tag flat-response, and the run states in as many words that the size check was never exercised. The same control catches the mirror-image case, where an endpoint returns the generic internal error to requests it never parsed.

    Recognized non-diagnostic answers

    A NetScaler SAML endpoint has a large set of possible replies, and only two of them establish patch state. The tool recognizes 20 of the others and names the condition on the per-route line rather than echoing a response body, for example:

    root@kitploit:~
         idp /saml/login      HTTP 200 "post body over the appliance's maximum": a different length limit rejected the probe first
         sp  /cgi/samlauth    HTTP 200 "assertion rejected before the size check": a different SAML condition, not the size check
    

    Three of them are length limits — an oversized post body, an oversized RelayState, an over-long extracted username. Those matter most, because they mean the request was rejected by a different length check before reaching the one that distinguishes patch state. That usually means the probe needs to be pointed at a different virtual server, not that the appliance is fine.

    One of the 20 shows up on every IdP-route run: the control's message timestamp outside the appliance's skew tolerance. The probe carries a fixed IssueInstant, so a control that clears the size check is then rejected for its age — a property of the probe, not of the appliance. A reply outside the set of 20 is printed as its own first line instead of a named condition.

    All of them still produce INCONCLUSIVE, tagged other-saml-error. Recognizing a response never upgrades it to PATCHED: only an explicit patched-build answer does that, and every unrecognized reply falls through to INCONCLUSIVE as well — as unrecognized-reply. The recognition exists to tell an operator why a target could not be classified, not to classify it.

    Exit codes

    CodeMeaning
    0Patched, or unaffected on the targeted virtual server
    1At least one target is VULNERABLE
    2Usage error (bad arguments / unreadable targets file)
    3At least one target is INCONCLUSIVE, none vulnerable
    4At least one target errored, none vulnerable or inconclusive

    2 is argparse's own exit code for a bad invocation, so the verdict codes skip it. A wrapper script can therefore tell "this appliance could not be classified" (3) from "I called the tool wrong" (2), which a scheme that overloaded 2 could not.

    Across a multi-target sweep the code is chosen by priority, not by the worst status: VULNERABLE > INCONCLUSIVE > ERROR > clean. An unreachable host therefore never masks a vulnerable finding in the exit code.

    Limitations

    • This checks one CVE, not the appliance's patch level. PATCHED and exit code 0 mean the CVE-2026-8452 size check is present on the path the probe reached. They say nothing about any other NetScaler vulnerability, including ones disclosed after this bug and fixed in later builds. Do not read exit 0 from this tool as a clean bill of health for an appliance.
    • A non-vulnerable verdict is scoped to the endpoint you probed. The precondition is per virtual server. UNAFFECTED means "not reachable here", not "this appliance is safe".
    • Policy matching gates the parser. /saml/login is appliance-wide, but on a virtual server with no IdP policy bound it answers Matching policy not found and short-circuits ahead of the canonicalizer. An IdP whose policy uses an expression rule that the probe's request does not satisfy will land in INCONCLUSIVE rather than give an answer.
    • INCONCLUSIVE is not a clean bill of health. It is deliberately distinguished from PATCHED with a separate exit code so that silence is never mistaken for a passing result.
    • nFactor deployments with SAML behind a first factor report INCONCLUSIVE. Where the SAML policy sits in a policy label reached by nextFactor rather than bound directly to the virtual server, an unsolicited assertion finds no matching policy and short-circuits ahead of the canonicalizer. Verified on a vulnerable build, which reported INCONCLUSIVE. Since SAML behind a device-posture or login-schema first factor is a common pattern, treat INCONCLUSIVE on an nFactor gateway as "probably reachable, confirm with show ns version", not as a curiosity.
    • Not an exploitation check. A VULNERABLE verdict confirms the missing size check, which is the patch state. It does not measure how far an attacker could take the corruption on your build.
    • Reachability only. A result reflects what the appliance exposes to the network position you run it from. A WAF in front of the appliance can mask the response.

    Remediation

    Upgrade to 13.1-63.21 or later, or 14.1-73.32 or later (FIPS and NDcPP: 14.1-73.32 FIPS, or 13.1-37.277 for 13.1-FIPS and 13.1-NDcPP).

    The fix for CVE-2026-8452 itself first shipped in 13.1-63.18 / 14.1-72.61 per CTX696604, and that is the transition this tool detects. Those builds have since been superseded by CTX696939 (2026-08-19), which adds CVE-2026-19489 and CVE-2026-19490, the latter a CVSS 9.3 pre-authentication authentication bypass. Its precondition on builds from 14.1-43.56 / 13.1-61.28 onward is a configured SAML action — so an appliance in scope for the bug this tool checks is likely in scope for that one too, and a PATCHED verdict here is not a reason to defer the upgrade. Both bulletins are cleared by the builds above.

    Appliances on 12.1 or 13.0 have no fix and will not receive one — those branches are end of life and should be treated as permanently vulnerable and migrated to a supported branch.

    Two further notes:

    • Patch both nodes of an HA pair. An unpatched secondary is a fully exposed appliance the moment it takes over.

    • Scope your inventory by SAML configuration, not by virtual server type. The vendor advisory's wording (Gateway or AAA virtual server) is broader than the trigger condition. Check the running configuration for add authentication samlAction and add authentication samlIdPProfile alongside add authentication vserver and add vpn vserver.

      This is tested, not inferred. On a confirmed-vulnerable appliance we removed every SAML object, bound a non-SAML authentication factor in its place, and left the AAA virtual servers up and serving: the SAML endpoints then returned 404 to every request. They are not merely policy-gated without SAML configuration — they do not exist. So a virtual server with no SAML on it is genuinely out of scope for this bug, and UNAFFECTED on such a target is a real answer rather than a blind spot. The caveat that still applies is the scoping one above: it is per virtual server, so confirm each VIP rather than concluding anything about the appliance.

    CVE-2026-8452 shipped alongside five siblings in the same bulletin. The one worth tracking next to it is CVE-2026-8451, a pre-authentication memory overread in the SAML IdP path that has seen active exploitation in the wild. The two share an attack surface, so the same configuration audit covers 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 CTX696604 — NetScaler security bulletin
    • Citrix CTX696939 — later NetScaler bulletin superseding those fixed builds
    • watchTowr Labs — CVE-2026-8452 technical analysis
    • NVD — CVE-2026-8452