漏洞名称:ApacheTomcat存在远程代码执行漏洞
漏洞编号:CVE-2024-56337
漏洞类型:文件上传、远程代码执行
漏洞威胁等级:高
影响范围:
影响范围:
1、readonly 初始化参数设置为false;
2、Tomcat 运行在区分大小写的文件系统上(Windows或某些Linux文件系统);
3、Java属性sun.io.useCanonCaches为true(Java8和Java 11默认为 true,Java17 默认false,Java21及以上版本不受影响)
影响版本
受影响版本
利用条件:目标服务器开启PUT
Tomcat是一个开源、免费、轻量级的Web服务器。
Tomcat是Apache软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。由于有了Sun 的参与和支持,最新的Servlet 和JSP 规范总是能在Tomcat 中得到体现,Tomcat 5支持最新的Servlet 2.4 和JSP 2.0 规范。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为比较流行的Web 应用服务器。
Apache Tomcat 远程代码执行漏洞(CVE-2024-56337), 该漏洞是由于CVE-2024-50379修复不完善,在不区分大小写的文件系统(如Windows)上,readonly参数被设置为false(非默认配置)且系统属性sun.io.useCanonCaches为true(Java 8或Java 11默认为true、Java 17默认为false、Java 21 及更高版本不受影响),攻击者就可以上传含有恶意JSP代码的文件。通过不断地发送请求利用条件竞争,使得Tomcat解析并执行这些恶意文件,从而实现远程代码执行。
http
为了提高成功率,此漏洞使用虚拟机win7搭建tomcat9.0.50版本,jdk1.8版本
此漏洞的复现步骤才用win7复现,首先在win7上搭建了tomcat的9.0.50版本以及jdk1.8版本并设置了readonly操作为false


抓包

其实和CVE-2024-50379的步骤一样只不过本次为了全面性考虑。考虑了目标主机在不出网以及持久性操作采用了两种方式
1:上传执行计算器的jsp代码,首先使用GET请求并发,通过CVE-2025-50379的修复补丁的代码分析利用锁的获取 / 释放与锁对象的生命周期管理(引用计数)被分离在不同的同步块中,导致状态不一致的思路让cache中存取www.jsp

2.在并发同时PUT上传payload文件,在某一时刻成功执行



深入利用:利用jsp重命名目标Jsp为jsp或者上传webshell或者内存马


成功写入内存


第二种方式:上传两个文件,使用其中jsp修改后缀实现持久加载jsp
<%@ page import="java.io.*" %>
<%
// 定义路径
String basePath = application.getRealPath("/");
String sourceFilePath = basePath + "1.txt";
String destinationFilePath = basePath + "2.Jsp";
BufferedReader reader = null;
BufferedWriter writer = null;
try {
File sourceFile = new File(sourceFilePath);
if (!sourceFile.exists()) {
out.println("Error: Source file does not exist.");
} else {
reader = new BufferedReader(new FileReader(sourceFile));
writer = new BufferedWriter(new FileWriter(destinationFilePath));
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
out.println("SAVED_PATH: " + destinationFilePath);
}
} catch (IOException e) {
out.println("Error: " + e.getMessage());
} finally {
// 安全关闭资源
if (reader != null) try { reader.close(); } catch (IOException ignored) {}
if (writer != null) try { writer.close(); } catch (IOException ignored) {}
}
%>
tomcat jdk
根据官方修复文档补丁
https://github.com/apache/tomcat/commit/43b507ebac9d268b1ea3d908e296cc6e46795c00
https://github.com/apache/tomcat/commit/631500b0c9b2a2a2abb707e3de2e10a5936e5d41
从代码分析如下:
首先,官方为了修复CVE-2024-56337,增加了锁机制,当读或者写入时存在冲突,因此以下代码增加了key。创建了锁对象并增加计数,之后同步了块外获取了读或者写的锁
@Override
public ResourceLock lockForRead(String path) {
String key = getLockKey(path);
ResourceLock resourceLock = null;
synchronized (resourceLocksByPathLock) {
/*
* Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has
* a consistent view of the currently "in-use" ResourceLocks.
*/
resourceLock = resourceLocksByPath.get(key);
if (resourceLock == null) {
resourceLock = new ResourceLock(key);
}
resourceLock.count.incrementAndGet();
}
// Obtain the lock outside the sync as it will block if there is a current write lock.
resourceLock.reentrantLock.readLock().lock();
return resourceLock;
}
并且在如下代码中可看出,官方考虑了大小写情况,当PUT 一个111.Jsp时与读取111.jsp利用了同一个key,发生冲突
private boolean isCaseSensitive() {
try {
String canonicalPath = getFileBase().getCanonicalPath();
File upper = new File(canonicalPath.toUpperCase(Locale.ENGLISH));
if (!canonicalPath.equals(upper.getCanonicalPath())) {
return true;
}
File lower = new File(canonicalPath.toLowerCase(Locale.ENGLISH));
if (!canonicalPath.equals(lower.getCanonicalPath())) {
return true;
}
/*
* Both upper and lower case versions of the current fileBase have the same canonical path so the file
* system must be case insensitive.
*/
} catch (IOException ioe) {
log.warn(sm.getString("dirResourceSet.isCaseSensitive.fail", getFileBase().getAbsolutePath()), ioe);
}
return false;
}
private String getLockKey(String path) {
// Normalize path to ensure that the same key is used for the same path.
String normalisedPath = RequestUtil.normalize(path);
if (caseSensitive) {
return normalisedPath;
}
return normalisedPath.toLowerCase(Locale.ENGLISH);
}
如下代码同步了块外释放实际锁并且减少计数移除锁对象
@Override
public ResourceLock lockForRead(String path) {
String key = getLockKey(path);
ResourceLock resourceLock = null;
synchronized (resourceLocksByPathLock) {
/*
* Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has
* a consistent view of the currently "in-use" ResourceLocks.
*/
resourceLock = resourceLocksByPath.get(key);
if (resourceLock == null) {
resourceLock = new ResourceLock(key);
}
resourceLock.count.incrementAndGet();
}
// Obtain the lock outside the sync as it will block if there is a current write lock.
resourceLock.reentrantLock.readLock().lock();
return resourceLock;
}
@Override
public void unlockForRead(ResourceLock resourceLock) {
// Unlock outside the sync as there is no need to do it inside.
resourceLock.reentrantLock.readLock().unlock();
synchronized (resourceLocksByPathLock) {
/*
* Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that
* map always has a consistent view of the currently "in-use" ResourceLocks.
*/
if (resourceLock.count.decrementAndGet() == 0) {
resourceLocksByPath.remove(resourceLock.key);
}
}
}
@Override
public ResourceLock lockForWrite(String path) {
String key = getLockKey(path);
ResourceLock resourceLock = null;
synchronized (resourceLocksByPathLock) {
/*
* Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has
* a consistent view of the currently "in-use" ResourceLocks.
*/
resourceLock = resourceLocksByPath.get(key);
if (resourceLock == null) {
resourceLock = new ResourceLock(key);
}
resourceLock.count.incrementAndGet();
}
// Obtain the lock outside the sync as it will block if there are any other current locks.
resourceLock.reentrantLock.writeLock().lock();
return resourceLock;
}
@Override
public void unlockForWrite(ResourceLock resourceLock) {
// Unlock outside the sync as there is no need to do it inside.
resourceLock.reentrantLock.writeLock().unlock();
synchronized (resourceLocksByPathLock) {
/*
* Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that
* map always has a consistent view of the currently "in-use" ResourceLocks.
*/
if (resourceLock.count.decrementAndGet() == 0) {
resourceLocksByPath.remove(resourceLock.key);
}
}
}
漏洞根本成因:如下Lock对象没有正确复用,没有放入map,当两个线程同时访问一个路径(GET和PUT),都没有命中缓存,则会各自创建自己的Resource实例,直接Lock split了,加锁失败。虽然加了 count.incrementAndGet(),但 resourceLocksByPath.put(key, resourceLock) 未调用,导致同一资源路径用的是不同的锁。那么就会导致如果大量请求访问目标jsp,就会在缓存中存储导致解析

随便访问不存在目录,利用404回显信息查看目标版本,例如:
本插件编写分为3个步骤,首先利用windows大小写不敏感机制上传Jsp绕过限制利用回显201响应码证明上传成功
再利用竞争条件访问目标jsp文件触发payload
利用目标请求协议以及响应包回显
升级 Apache Tomcat版本至:
Java8/Java11:设置系统属性sun.io.useCanonCaches 为false(默认值为 true)。
Java17:如该属性被设置,需确保其值为false(默认值为 false)。
Java21 及以上版本:无需配置(该属性已被移除)。