ApacheTomcat存在远程代码执行漏洞(CVE-2024-56337)
Apache Tomcat 存在远程代码执行漏洞(CVE-2024-56337),影响多个版本,在不区分大小写的文件系统上利用条件竞争上传恶意JSP文件并执行。修复需升级至指定版本或调整Java属性。 2025-8-22 05:56:3 Author: www.freebuf.com(查看原文) 阅读量:26 收藏

一、漏洞概述

1.1漏洞简介

  • 漏洞名称:ApacheTomcat存在远程代码执行漏洞

  • 漏洞编号:CVE-2024-56337

  • 漏洞类型:文件上传、远程代码执行

  • 漏洞威胁等级:高

  • 影响范围:

    • 影响范围:

      1、readonly 初始化参数设置为false;

      2、Tomcat 运行在区分大小写的文件系统上(Windows或某些Linux文件系统);

      3、Java属性sun.io.useCanonCaches为true(Java8和Java 11默认为 true,Java17 默认false,Java21及以上版本不受影响)

      影响版本

      • Apache Tomcat 11.0.0-M1 至 11.0.1
      • Apache Tomcat 10.1.0-M1 至 10.1.33
      • Apache Tomcat 9.0.0-M1 至 9.0.97

      受影响版本

      • 11.0.0-M1 <= Apache Tomcat <= 11.0.1
      • 10.1.0-M1 <= Apache Tomcat <= 10.1.33
      • 9.0.0.M1 <= Apache Tomcat <= 9.0.97
    • 利用条件:目标服务器开启PUT

1.2组件描述

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 应用服务器。

1.3漏洞描述

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解析并执行这些恶意文件,从而实现远程代码执行。

二、漏洞复现

2.1 应用协议

http

2.2 环境搭建

为了提高成功率,此漏洞使用虚拟机win7搭建tomcat9.0.50版本,jdk1.8版本

2.3 漏洞复现

此漏洞的复现步骤才用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) {}
    }
%>

三、漏洞分析

3.1 技术背景

tomcat jdk

3.1 代码分析

根据官方修复文档补丁

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,就会在缓存中存储导致解析​

四、漏洞检测

4.1 组件版本自检

随便访问不存在目录,利用404回显信息查看目标版本,例如:

4.2 漏洞检测规则、插件编写思路

本插件编写分为3个步骤,首先利用windows大小写不敏感机制上传Jsp绕过限制利用回显201响应码证明上传成功

再利用竞争条件访问目标jsp文件触发payload

4.3 研判建议

利用目标请求协议以及响应包回显

五、防范建议

5.1 官方修复建议

升级 Apache Tomcat版本至:

  • Apache Tomcat 11.0.2 及以上
  • Apache Tomcat 10.1.34 及以上
  • Apache Tomcat 9.0.98 及以上

5.2 临时修复建议

Java8/Java11:设置系统属性sun.io.useCanonCaches​​ 为false(默认值为 true)。

Java17:如该属性被设置,需确保其值为false(默认值为 false)。

Java21 及以上版本:无需配置(该属性已被移除)。


文章来源: https://www.freebuf.com/articles/vuls/445404.html
如有侵权请联系:admin#unsafe.sh