描述:Apache Tomcat 是一个广泛使用的开源Java Servlet、JavaServer Pages、Java Expression Language和WebSocket技术的实现。在 Tomcat 版本 9.x ~ 9.0.97,10.x ~ 10.1.34, 11.x ~ 11.0.2 中,当 Tomcat 同时配置了可写的 DefaultServlet(readonly=false)和基于文件的会话持久化时,攻击者可以向服务器写入任意文件,并通过操作 JSESSIONID cookie 触发这些文件的反序列化,最终导致远程代码执行。
Tomcat链路
Tomcat 接收到 HTTP 请求开始链路如下:
| 组件 | 作用 | 与漏洞的关系 |
|---|---|---|
| Connector | 监听端口,解析 HTTP 协议 | 接收 PUT 请求 |
| Engine/Host/Context | 虚拟主机和应用上下文 | 确定请求归属哪个 Web 应用 |
| Wrapper | Servlet 的包装器 | 将请求路由到 DefaultServlet |
| DefaultServlet | 处理静态资源的默认 Servlet | 漏洞发生在这里 |
DefaultServlet 处理 PUT 的核心代码逻辑
// org.apache.catalina.servlets.DefaultServlet
protected void doPut(HttpServletRequest req, HttpServletResponse resp) {
// 1. 检查 readonly 配置
if (readOnly) {
sendError(resp, HttpServletResponse.SC_FORBIDDEN);
return;
}
// 2. 解析请求路径
String path = getRelativePath(req);
// 3. 检查是否是 Partial PUT(通过 Content-Range 头判断)
if (req.getHeader("Content-Range") != null) {
// 进入 Partial PUT 处理分支
executePartialPut(req, path);
} else {
// 普通 PUT 处理
executePut(req, path);
}
Partial PUT 的核心处理(漏洞触发点)
Tomcat 为了支持断点续传式的文件上传,实现了 Partial PUT:
protected void executePartialPut(HttpServletRequest req, String path) {
// 关键:生成临时文件名
// 原代码存在漏洞:将路径中的 "/" 替换为 "."
String tempFileName = path.replace('/', '.');
// 生成的临时文件路径
File tempFile = new File(tempDir, tempFileName);
// 将上传的数据写入临时文件
// ...
// 后续会将临时文件内容合并到目标文件
}
攻击者输入:
PUT /uploads/../../work/Catalina/localhost/ROOT/.evil.session
Content-Range: bytes 0-1000/1001
Tomcat 处理后:
path = /uploads/../../work/Catalina/localhost/ROOT/.evil.session
tempFileName = .uploads....work.Catalina.localhost.ROOT..evil.session
但实际的文件写入逻辑可能因路径解析而到达 Session 目录
攻击者利用 Tomcat 的临时文件目录和 Session 目录的相对位置关系,通过构造特定的路径,让临时文件恰好落在 Session 存储目录中。
Session 持久化机制(FileStore)
Tomcat 默认使用内存存储 Session(StandardManager),服务器重启后 Session 丢失。PersistentManager 可以将 Session 保存到磁盘或数据库,实现:
- 服务器重启后 Session 不丢失
- 内存不足时将不活跃的 Session 换出到磁盘
配置:
<Manager className="org.apache.catalina.session.PersistentManager">
<Store className="org.apache.catalina.session.FileStore"
directory="/path/to/sessions"/>
</Manager>
Session存储流程:
用户登录 ──▶ 创建 Session 对象 ──▶ 序列化为字节流 ──▶ 写入 .session 文件
Session 读取流程:
请求携带 JSESSIONID ──▶ 查找 .session 文件 ──▶ 反序列化 ──▶ 恢复 Session 对象
*Session 文件的命名规则
JSESSIONID=abc123 ──▶ 查找 abc123.session 文件
JSESSIONID=.evil ──▶ 查找 .evil.session 文件

