Threat Overview
On September 27, 2026, The Hacker News reported that watchTowr had identified two previously undisclosed remote-code-execution vulnerabilities affecting Citrix NetScaler ADC and NetScaler Gateway during forensic investigations. watchTowr characterized both flaws as unpatched and exploited before fixes existed. As of the report, Citrix had not published a security bulletin, assigned CVEs, confirmed affected builds, disclosed exploit paths, or released indicators of compromise. This is a high-consequence edge-device warning, but the available public evidence remains incomplete. The report should therefore drive emergency exposure validation and compromise assessment, not unsupported signature claims.
References
- The Hacker News, September 27, 2026: https://thehackernews.com/2026/09/warning-two-unpatched-citrix-netscaler.html
- Citrix, Steps to Take if NetScaler ADC Is Suspected to Be Compromised: https://support.citrix.com/external/article/CTX694799/steps-to-take-if-netscaler-adc-is-suspected-to-be-compromised.html
Impacted Systems
- Vendor/product: Citrix NetScaler ADC and NetScaler Gateway.
- Role: Internet-facing application delivery, VPN/remote access, load balancing, and authentication edge appliances.
- Deployment: Physical MPX, SDX-hosted VPX, and standalone VPX may be relevant; the public report does not confirm an affected deployment model.
- Affected versions/builds: Not confirmed. Citrix had not stated whether August builds 14.1-73.32 and 13.1-63.21 or later builds are affected.
- Exploit prerequisite: Reported as remote code execution; authentication and configuration prerequisites are not publicly confirmed.
- Exposure: Internet-reachable NetScaler services present the highest concern. Management services should not be Internet-exposed.
- Fixed versions: None confirmed as of September 27, 2026.
- Distinction: These reported zero-days are separate from CVE-2026-19490, which Citrix fixed on August 19, 2026.
Why this matters
NetScaler appliances commonly terminate remote access and authentication at the enterprise perimeter. Successful code execution can expose credentials, session material, certificates, private keys, backend connectivity, and an opportunity to persist on a device that conventional endpoint agents may not monitor.
Exploitation Status
- Researcher statement: watchTowr reported two RCE flaws discovered during forensic investigations and said both had been exploited before fixes existed.
- Vendor status: Citrix had not confirmed the flaws or published patches, affected-version guidance, workarounds, or IOCs as of the reporting cutoff.
- Evidence limitation: No victim, exploit request, file path, process name, source address, or artifact was publicly substantiated. Claims beyond reported active exploitation would be speculative.
What this hunt looks for
The hunt content is designed to review available Microsoft Sentinel telemetry for NetScaler log presence, new or rare administrative sources, suspicious appliance log terms, management-plane connections, unusual outbound appliance traffic, and authentication anomalies adjacent to NetScaler use.
Required logs
Conclusions are limited to data sources and retention available in Microsoft Sentinel. NetScaler appliance syslog, NetScaler Console logs, firewall/flow telemetry, and identity telemetry must be ingested for relevant visibility. Absence of matching activity does not confirm absence of exploitation, especially because the exploit path and IOCs are not public and the appliance may not forward forensic artifacts.
1. First Pass – NetScaler Appliance Log Activity and High-Risk Terms
let Lookback = 14d;
union isfuzzy=true
(
Syslog
| where TimeGenerated >= ago(Lookback)
| extend Device=tostring(Computer), Message=tostring(SyslogMessage), SourceTable="Syslog"
),
(
CommonSecurityLog
| where TimeGenerated >= ago(Lookback)
| extend Device=coalesce(tostring(DeviceName), tostring(DestinationHostName)), Message=strcat(tostring(Activity), " ", tostring(Message), " ", tostring(DeviceCustomString1), " ", tostring(DeviceCustomString2)), SourceTable="CommonSecurityLog"
)
| where Message has_any ("NetScaler", "Citrix ADC", "NSCONFIG", "ns.conf", "NITRO", "shell", "command", "unauthorized", "authentication failure", "login failed")
| project TimeGenerated, SourceTable, Device, Message
| order by TimeGenerated desc2. New or Rare Source Addresses in NetScaler-Related Logs
let BaselineStart = ago(30d);
let RecentStart = ago(48h);
let Logs = union isfuzzy=true
(
Syslog
| where TimeGenerated >= BaselineStart
| extend Message=tostring(SyslogMessage), SrcIP=extract(@"(?i)(?:src|source|client|remote)(?:ip)?[=: ]+([0-9]{1,3}(?:\.[0-9]{1,3}){3})", 1, SyslogMessage), Device=tostring(Computer)
),
(
CommonSecurityLog
| where TimeGenerated >= BaselineStart
| extend Message=strcat(tostring(Activity), " ", tostring(Message)), SrcIP=tostring(SourceIP), Device=coalesce(tostring(DeviceName), tostring(DestinationHostName))
)
| where Message has_any ("NetScaler", "Citrix ADC", "Gateway", "AAA", "NITRO");
let Baseline = Logs | where TimeGenerated < RecentStart | where isnotempty(SrcIP) | summarize by SrcIP;
Logs
| where TimeGenerated >= RecentStart and isnotempty(SrcIP)
| join kind=leftanti Baseline on SrcIP
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Events=count(), Devices=make_set(Device, 20), Samples=make_set(Message, 5) by SrcIP
| order by Events desc3. NetScaler Administrative or Configuration Events
union isfuzzy=true
(
Syslog
| where TimeGenerated >= ago(14d)
| extend Device=tostring(Computer), Message=tostring(SyslogMessage), SrcIP=extract(@"([0-9]{1,3}(?:\.[0-9]{1,3}){3})", 1, SyslogMessage)
),
(
CommonSecurityLog
| where TimeGenerated >= ago(14d)
| extend Device=coalesce(tostring(DeviceName), tostring(DestinationHostName)), Message=strcat(tostring(Activity), " ", tostring(Message), " ", tostring(DeviceCustomString1), " ", tostring(DeviceCustomString2)), SrcIP=tostring(SourceIP)
)
| where Message has_any ("NITRO", "CONFIG", "COMMAND", "CLI", "SSH", "SCP", "SFTP", "ns.conf", "add system user", "save ns config", "shell")
| project TimeGenerated, Device, SrcIP, Message
| order by TimeGenerated desc4. Connections to NetScaler Management Interfaces
let NetScalerIPs = dynamic(["REPLACE_WITH_NETSCALER_IP"]);
CommonSecurityLog
| where TimeGenerated >= ago(14d)
| where DestinationIP in (NetScalerIPs)
| where DestinationPort in (22, 80, 443, 3010)
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Connections=count(), Actions=make_set(DeviceAction, 10) by SourceIP, DestinationIP, DestinationPort, DeviceVendor, DeviceProduct
| order by Connections desc5. Unusual Outbound Traffic From NetScaler Appliances
let NetScalerIPs = dynamic(["REPLACE_WITH_NETSCALER_IP"]);
let BaselineStart = ago(30d);
let RecentStart = ago(48h);
let Baseline = CommonSecurityLog
| where TimeGenerated between (BaselineStart .. RecentStart)
| where SourceIP in (NetScalerIPs)
| summarize by DestinationIP, DestinationPort;
CommonSecurityLog
| where TimeGenerated >= RecentStart
| where SourceIP in (NetScalerIPs)
| join kind=leftanti Baseline on DestinationIP, DestinationPort
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Connections=count(), BytesOut=sum(tolong(SentBytes)), Actions=make_set(DeviceAction, 10) by SourceIP, DestinationIP, DestinationPort
| order by Connections desc6. Authentication Failures Followed by Success From the Same Source
let Window = 30m;
let Failures = Syslog
| where TimeGenerated >= ago(14d)
| where SyslogMessage has_any ("NetScaler", "AAA", "Gateway")
| where SyslogMessage has_any ("fail", "denied", "invalid")
| extend SrcIP=extract(@"([0-9]{1,3}(?:\.[0-9]{1,3}){3})", 1, SyslogMessage)
| where isnotempty(SrcIP)
| project FailureTime=TimeGenerated, Computer, SrcIP, FailureMessage=SyslogMessage;
let Successes = Syslog
| where TimeGenerated >= ago(14d)
| where SyslogMessage has_any ("NetScaler", "AAA", "Gateway")
| where SyslogMessage has_any ("success", "accepted", "authenticated")
| extend SrcIP=extract(@"([0-9]{1,3}(?:\.[0-9]{1,3}){3})", 1, SyslogMessage)
| where isnotempty(SrcIP)
| project SuccessTime=TimeGenerated, Computer, SrcIP, SuccessMessage=SyslogMessage;
Failures
| join kind=inner Successes on SrcIP, Computer
| where SuccessTime between (FailureTime .. FailureTime + Window)
| summarize FailedAttempts=count(), FirstFailure=min(FailureTime), SuccessTime=min(SuccessTime), FailureSamples=make_set(FailureMessage, 3), SuccessSamples=make_set(SuccessMessage, 3) by Computer, SrcIP
| order by FailedAttempts descDetection Notes
- Query 1 is the fastest inventory and triage pass; it is not an exploit signature.
- Queries 4 and 5 require the actual NetScaler IP addresses to replace the placeholder.
- Raw NetScaler messages vary by firmware, feature, and connector. Validate local message formats before relying on parsed source addresses or success/failure keywords.
- The absence of published exploit paths and IOCs sharply limits signature-based detection. Appliance snapshots, support bundles, core dumps, and local filesystem review are outside Sentinel unless their results are ingested.
- Expected noise includes legitimate administrators, configuration automation, vulnerability scanners, health checks, and normal backend traffic.