Skip to content

feat(citrix-honeypot): KEV-metadata coverage for the NetScaler zero-day RCE pair (#3467) - #3469

Merged
Xore merged 2 commits into
mainfrom
oc/3467-netscaler-cve
Sep 28, 2026
Merged

Xore merged 2 commits into
mainfrom
oc/3467-netscaler-cve

Conversation

@Xore

@Xore Xore commented Sep 28, 2026

Copy link
Copy Markdown
Owner

feat(citrix-honeypot): KEV-metadata coverage for the NetScaler zero-day RCE pair (#3467)

CVE-2026-88771 and CVE-2026-88772 are a Citrix NetScaler ADC/Gateway
zero-day RCE pair, added to CISA KEV on 2026-09-27, each independently
sufficient for RCE. This is detection coverage for citrix-honeypot, and
it is deliberately narrower than the issue's Signal A/B/C sketch.

There is no payload signature in this change, because none is published.
CTX697096 and the KEV catalog give preconditions, CVSS vectors, CWEs and
fixed builds, and nothing else: no path, method, header, parameter, body
field or byte sequence for either CVE. The "provided IOCs" both sources
point at are, per watchTowr, run as an IOC scan on the NetScaler console
advisory page or requested from Citrix Support -- appliance-console
artifacts gated on a build this decoy does not run, not wire patterns.
Shipping an invented signature here would produce detection events an
analyst trusts and that correspond to nothing, so the deliverable is
KEV-metadata-driven coverage instead.

Verified this run against the live KEV feed (catalog 2026.09.27, 1728
entries; both CVEs dateAdded 2026-09-27, dueDate 2026-09-30,
forensicTriage Yes) and against CTX697096.

What ships:

  • netscaler_kev_exposure, once per process start, one event per CVE
    carrying the verified KEV/bulletin metadata and a
    decoy_exercises_precondition field. The CVE id goes in path so
    path: "CVE-2026-88771" is a working query. Uses the existing emit
    path; no new fields on the shared event struct.

  • netscaler_cmd_metachar_shape_inferred, an INFERRED classifier over the
    one documented primitive ("an unauthenticated attacker to execute
    arbitrary commands"). The primitive is documented; where the
    unvalidated input lands is not, so the event name carries "inferred"
    and the code comment says so. Two tiers: high-conviction tokens
    (backtick, $(, ${, LF, CR) fire alone; low-conviction ones
    (&&, ||, ;, |, >, <, &) need two distinct, because & is the query
    separator and ; is the Jetty/Tomcat matrix-parameter form. A bare
    "/vpn/;id" deliberately does not classify -- it is indistinguishable
    from "/store;jsessionid=..." by shape alone, and that miss is recorded
    in a comment rather than left to be discovered.

CVE-2026-88772 has no classifier. Its precondition is DTLS, a UDP
transport, and this decoy is TCP-only (net.Listen("tcp") behind
tls.NewListener, fronted tcp:4443 -> 10.8.0.2:443:pp), so the
precondition is structurally unobservable here. The gap is recorded as
queryable data in a precondition_gap field rather than papered over.

Also recorded: KEV lists cwes ["CWE-119"] for both entries, contradicting
CTX697096's CWE-20 for 88771. The code uses the vendor's own
classification of its own CVE and notes the discrepancy in a comment.

Tests: every classifier has a CAN-fire and a does-not-fire test, the
benign corpus drawn from traffic this decoy actually serves (SAML
wctx/SAMLRequest base64, JWT bearer, a=1&b=2, /store;jsessionid=,
newbm.pl, an ordinary credential POST). Four mutants run and reverted:
emptying the high tier (7 CAN-fire failures), reverting to per-token
strings.Contains (5 benign failures -- the Contains scan made "a=1&&b=2"
match both & and && and fire on the very artifact the low tier exists to
tolerate), moving the classifier after the early-returning auth block
(the ordering test fails), and bolting a fabricated payload= field into
the KEV metadata (the anti-fabrication guard fails on both CVEs).

Out of scope, noted in the file and the doc rather than coded: 88772
needs a UDP/DTLS listener, which is a new exposed surface and its own
issue; 88773 request smuggling belongs in http-honeypot per #3464;
88774-88778 are configuration-dependent and not reported exploited;
a "no preceding session" classifier needs per-source state this package
does not hold.

Not verified: nothing deployed, so no live detection rate is claimable.
The Citrix IoC blog section could not be read (community.citrix.com
returns 403 to fetch and to a browser-UA curl), so the IoC conclusion
rests on watchTowr's description of where those IOCs live, not on having
read the list. No live Suricata ruleset re-audit. No traffic sent
anywhere: all fixtures are inert strings in unit tests.

@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

…ay RCE pair (#3467)

CVE-2026-88771 and CVE-2026-88772 are a Citrix NetScaler ADC/Gateway
zero-day RCE pair, added to CISA KEV on 2026-09-27, each independently
sufficient for RCE. This is detection coverage for citrix-honeypot, and
it is deliberately narrower than the issue's Signal A/B/C sketch.

There is no payload signature in this change, because none is published.
CTX697096 and the KEV catalog give preconditions, CVSS vectors, CWEs and
fixed builds, and nothing else: no path, method, header, parameter, body
field or byte sequence for either CVE. The "provided IOCs" both sources
point at are, per watchTowr, run as an IOC scan on the NetScaler console
advisory page or requested from Citrix Support -- appliance-console
artifacts gated on a build this decoy does not run, not wire patterns.
Shipping an invented signature here would produce detection events an
analyst trusts and that correspond to nothing, so the deliverable is
KEV-metadata-driven coverage instead.

Verified this run against the live KEV feed (catalog 2026.09.27, 1728
entries; both CVEs dateAdded 2026-09-27, dueDate 2026-09-30,
forensicTriage Yes) and against CTX697096.

What ships:

- netscaler_kev_exposure, once per process start, one event per CVE
  carrying the verified KEV/bulletin metadata and a
  decoy_exercises_precondition field. The CVE id goes in `path` so
  `path: "CVE-2026-88771"` is a working query. Uses the existing emit
  path; no new fields on the shared event struct.

- netscaler_cmd_metachar_shape_inferred, an INFERRED classifier over the
  one documented primitive ("an unauthenticated attacker to execute
  arbitrary commands"). The primitive is documented; where the
  unvalidated input lands is not, so the event name carries "inferred"
  and the code comment says so. Two tiers: high-conviction tokens
  (backtick, $(, ${, LF, CR) fire alone; low-conviction ones
  (&&, ||, ;, |, >, <, &) need two distinct, because & is the query
  separator and ; is the Jetty/Tomcat matrix-parameter form. A bare
  "/vpn/;id" deliberately does not classify -- it is indistinguishable
  from "/store;jsessionid=..." by shape alone, and that miss is recorded
  in a comment rather than left to be discovered.

CVE-2026-88772 has no classifier. Its precondition is DTLS, a UDP
transport, and this decoy is TCP-only (net.Listen("tcp") behind
tls.NewListener, fronted tcp:4443 -> 10.8.0.2:443:pp), so the
precondition is structurally unobservable here. The gap is recorded as
queryable data in a precondition_gap field rather than papered over.

Also recorded: KEV lists cwes ["CWE-119"] for both entries, contradicting
CTX697096's CWE-20 for 88771. The code uses the vendor's own
classification of its own CVE and notes the discrepancy in a comment.

Tests: every classifier has a CAN-fire and a does-not-fire test, the
benign corpus drawn from traffic this decoy actually serves (SAML
wctx/SAMLRequest base64, JWT bearer, a=1&b=2, /store;jsessionid=,
newbm.pl, an ordinary credential POST). Four mutants run and reverted:
emptying the high tier (7 CAN-fire failures), reverting to per-token
strings.Contains (5 benign failures -- the Contains scan made "a=1&&b=2"
match both & and && and fire on the very artifact the low tier exists to
tolerate), moving the classifier after the early-returning auth block
(the ordering test fails), and bolting a fabricated payload= field into
the KEV metadata (the anti-fabrication guard fails on both CVEs).

Out of scope, noted in the file and the doc rather than coded: 88772
needs a UDP/DTLS listener, which is a new exposed surface and its own
issue; 88773 request smuggling belongs in http-honeypot per #3464;
88774-88778 are configuration-dependent and not reported exploited;
a "no preceding session" classifier needs per-source state this package
does not hold.

Not verified: nothing deployed, so no live detection rate is claimable.
The Citrix IoC blog section could not be read (community.citrix.com
returns 403 to fetch and to a browser-UA curl), so the IoC conclusion
rests on watchTowr's description of where those IOCs live, not on having
read the list. No live Suricata ruleset re-audit. No traffic sent
anywhere: all fixtures are inert strings in unit tests.
@Xore
Xore force-pushed the oc/3467-netscaler-cve branch from 3fcc7f0 to c69eb86 Compare September 28, 2026 09:08
@Xore
Xore merged commit 50972ed into main Sep 28, 2026
116 of 118 checks passed
@Xore
Xore deleted the oc/3467-netscaler-cve branch September 28, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant