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:
- Dumpster fire, or,
- 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.
Date | Detail |
---|---|
28th March 2025 | WT-2025-0037 discovered and disclosed to Dell. |
28th March 2025 | watchTowr begins hunting for vulnerability across client attack surfaces. |
31st March 2025 | Dell confirms the receipt of the WT-2025-0037 report and opens a ticket VRT-29331. |
31st July 2025 | Security Advisory Published by Dell PSIRT followed by a new version (5.5.1) of the product. |
3rd October 2025 | Blog 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.