# It’s Never Simple Until It Is (Dell UnityVSA Pre-Auth Command Injection CVE-2025-36604)
Welcome back, and what a week! We’re glad that happened for you and/or sorry that happened to you. It will get better and/or worse, and you will likely survive.
Today, we’re walking down the garden path and digging into the archives, publishing our analysis of a vulnerability we discovered and disclosed to Dell in March 2025 within their UnityVSA solution.
As part of our continued enhancement of our Preemptive Exposure Management technology within the watchTowr Platform, we perform zero-day vulnerability research in technology that we see across the attack surfaces of organisations leveraging the watchTowr Platform. This enables proactive defence for our clients and provides forward visibility of vulnerabilities while we liaise with vendors and projects for suitable fixes.
In March, we reported a Pre-Auth Command Injection in Dell UnityVSA in version `5.5.0.0.5.259` (and we assume previous versions). As always, it’s easier to refer to this vulnerability as if we’re robots, using the lovingly and affectionately assigned CVE-2025-36604.
Dell UnityVSA (Virtual Storage Appliance) is the software-defined version of Dell’s Unity storage platform. Instead of running on dedicated Dell storage hardware, UnityVSA runs as a virtual machine (VM) on a hypervisor such as VMware ESXi, giving you most of the Unity features in a software-only package.
As we all know, storage and solutions surrounding the storage ecosystem are of material interest to Internet-dwellers, primarily and obviously because they provide a clear path to sensitive data that is either 1. usable or 2. ransomware-able.
With that uplifting and motivating fact, let’s begin…
### UnityVSA, Dell, And The 14 CVEs
To set the stage, let’s look at where UnityVSA stood after Dell’s patch disclosure – quickly punched in the face (nicely, probably) by the Dell security release notes for Dell Unity:
Someone had managed to find 14 (yes, fourteen!) Pre-Auth Command Injection vulnerabilities – and there were two ways we could take this information:
1. Dumpster fire, or,
2. This is now a cleaned-up, well-written and secure code base.
We had to know – the mystery and potential surprise were too alluring – and so we very quickly diff’d out the patched vulnerabilities.
We used;
– Dell UnityVSA 5.4.0.0.5.094 (known vulnerable)
– Dell UnityVSA 5.5.0.0.5.259 (presumably perfect with 14 comprehensive patches)
### AccessTool.pm
Among the diff results, one file in particular grabbed our attention.
`AccessTool.pm` stood there – mocking us, and likely cosplaying as the culprit for our patch-diffing focus:
A quick look through the Perl module showed multiple fixes – including a developer’s helpful reminder to themselves in the form of a comment about some pesky unwanted command injection concerns.
While this, of course, is not bad in itself, we have discussed the concept of ‘code smells’ – indicators that our brains turn into gut feelings. Let’s see.
The snippet lives inside a Perl function called `getCASURL`.
The diff shows what looks to inspired minds like a textbook Perl command injection – attacker-influenced values such as `$host` are stitched together into a single `$exec_cmd` string. In the patched version, those same inputs are wrapped in single quotes to try to neuter shell metacharacters.
The final kicker is that `$exec_cmd` gets run with the classic Perl backtick operator, so any escaping slip is all you need for remote command execution.
### getCASURL
At this point ,we asked ourselves: could there be more in this clearly strong code base?
Take another look at the screenshot above. Are you seeing what we’re seeing? Let’s zoom in.
It turns out that if `$type` is set to the literal value “login”, then at line 574 the `$uri` is concatenated straight into our `$exec_cmd` – the final command.
The obvious question: why isn’t this variable escaped?
Everywhere else, the developers took the time to escape inputs, but not here. Did they assume it wasn’t attacker-controlled? Or are we observing future job security?
Either way… sigh.
To understand how we arrive at the snippet above, it’s important to note that the code resides within a function named `getCASURL`:
To reach the section of code where `$uri` is concatenated, the final argument passed to `getCASURL` – `$type` – must equal the literal `”login”`. This is the single condition that gates the concatenation logic; if it isn’t satisfied, the code path in question will never be executed.
With that in mind, the next step is to review every caller of `getCASURL` to identify where `$type` is set to `”login”` (or can be influenced to become `”login”`). Mapping those call sites lets us separate theoretical paths from practically reachable ones and focus our analysis on the scenarios that matter.
With that gate condition set, the obvious next step is to track down who’s actually calling `getCASURL`:
There are two call sites for this function. If execution reaches `getCASLoginURL`, the final argument is set to the literal `”login”`, which satisfies the condition required to hit the `$uri` concatenation.
The next step is to continue the search and identify every caller of `getCASLoginURL`. Mapping those call sites will show when this path is exercised in practice and help us evaluate real-world reachability.
Tracing further, we find the call originates from `make_return_address()` – and this is where it gets interesting:
The path into `getCASLoginURL` originates at `make_return_address()`. That helper retrieves the inbound request URI via the standard Perl call `$r->uri()` and then calls `getCASLoginURL` with that value.
The plot thickens when we see how `AccessHandler.pm` makes use of it.
Continuing the trace, `make_return_address()` is called from `AccessHandler.pm`. The `handler()` function in that module checks at line 153 whether the request lacks a cookie. If no cookie is present, the user is considered unauthenticated and is redirected to the login page, which triggers a call to `make_return_address($r)` using the current HTTP context.
### Apache Configuration
Of course, none of this matters unless the handler is actually executed. The answer sits inside Apache’s configuration:
As we can see, Apache’s configuration registers the function it for the relevant request scope.
In `httpd.conf` we observed a configuration entry that routes matching requests to `AccessHandler::handler`, ensuring the module is invoked during request processing.
Practically, this means `handler()` runs for each incoming request that falls under that scope. When a request arrives without the expected cookie, the code triggers a redirect to the login flow by calling `make_return_address($r)`, which in turn leads to `getCASLoginURL(…)`. That path reaches the `$uri` concatenation only when `$type` equals `”login”`.
This effectively means the developers have registered a callback to be executed on every request by loading a Perl module with the `PerlModule` directive. As a result, `AccessHandler::handler` is invoked for each request handled by the web server.
That’s a few moving parts – let’s step back and map the flow:
“`
[HTTP Request] | v +——————————————-+ | Apache httpd.conf | | PerlModule MOD_SEC_EMC::AccessHandler | +——————————————-+ | v +—————————————-+ | MOD_SEC_EMC::AccessHandler::handler($r)| +—————————————-+ | v +—————————————-+ | AccessTool::make_return_address($r) | | – derives $uri_to_use_raw <———-|–[RAW user-controlled URI] +—————————————-+ | v +—————————————-+ | getCASLoginURL($r, $uri_to_use_raw) | | – forwards raw URI | +—————————————-+ | v +————————————————–+ | getCASURL($r, $uri=$uri_to_use_raw, $scheme, | | $type=”login”) | | … | | … | | … | | if ($type eq “login”) { | | $exec_cmd .= $uri; # concat raw URI | | my $output = `$exec_cmd`; # shell execution | | } | +————————————————–+
“`
Perfect.
This means we can exercise the login redirect path by issuing an HTTP request to any page that contains a valid Command Injection payload in the request URI. We must ensure the request carries no authentication cookie so the handler is triggered.
With the chain clear, the next question is obvious: what happens when we try it?
### It’s Never Simple Until It Is
At first, it looked like we had a clear win. But reality had other plans (as always):
Our first attempt fell flat: because the URI didn’t point to a valid resource, the Perl module never fired.
That neatly explains why this path may have slipped under the radar for so long – no resolution, no handler, no vuln.
But what if we hand it a URI that _does_ require resolution? In that case, the handler springs into action, and suddenly the code path we’ve been chasing might come alive.
### Detection Artefact Generator
As is mostly customary, we’re sharing our Detection Artefact Generator for this vulnerability – to enable teams to identify vulnerable hosts within their environments.
### **Timeline**
We’d like to thank Dell PSIRT for their professionalism and responsiveness in handling this security report.
DateDetail28th March 2025WT-2025-0037 discovered and disclosed to Dell.28th March 2025watchTowr begins hunting for vulnerability across client attack surfaces.31st March 2025Dell confirms the receipt of the WT-2025-0037 report and opens a ticket VRT-29331.31st July 2025Security Advisory Published by Dell PSIRT followed by a new version (5.5.1) of the product.3rd October 2025Blog post published.
The research published by watchTowr Labs is just a glimpse into what powers the watchTowr Platform – delivering automated, continuous testing against real attacker behaviour.
By combining Proactive Threat Intelligence and External Attack Surface Management into a single **Preemptive Exposure Management** capability, the watchTowr Platform helps organisations rapidly react to emerging threats – and gives them what matters most: **time to respond.**
