Tomcat CVE-2024-50379 / CVE-2024-56337 分析 修复 检测

0x01 漏洞描述

两个漏洞内容是一样的,只是 CVE-2024-50379 没有修复完全,CVE-2024-56337 做了二次修复

漏洞描述:

几个关键点:

 

0x02 调试环境

参考曾经 PUT RCE 需要打开 conf/web.xmlreadonlyfalse

 

启动 Tomcat 时使用 bin/catalina.batjpda 启动

 

lib/*.jar 文件使用 IDEA 右键 Add As Library 然后我们就可以动态调试了

注意 Tomcat 默认使用的是 8000 调试端口

 

0x03 初步分析

参考 0x01 中的几个关键点,猜测基本的漏洞原理:

接下来我们从 DefaultServlet 的入口 doGet 开始

跟入 serveResource 可以主要逻辑如下:

跟入 this.resources.getResource(path) 到达 StandardRoot#getRource

以上 DefaultServlet 负责静态资源处理,而 JspServlet 负责 jsp 处理和编译

其中 JspServlet 只处理 jsp/jspx 参考下图 org.apache.catalina.mapper.Mapper#internalMapWrapper

访问 a.JSPa.Jsp 等变形内容,都是由 DefaultServlet 处理

跟入 JspServlet#serviceJspFile 方法,进入 getResource 方法

调试后发现同样到达了 StandardRoot#getRource 方法,二者在寻找本地文件的代码相同

回到 StandardRoot 类,由于该类是两个 Servlet 共同寻找文件的方法,我们从该类开始深入分析,即可找到竞争的原因

在调用 this.cache.getResource 前判断是否开启了 cache

注意该功能文档位于:https://tomcat.apache.org/tomcat-9.0-doc/config/resources.html

主要内容是:

跟入 cache 类(这里无需过多关心,竞争的原理和这里的缓存无关)

注意第一次访问 favicon.ico 得到的是空

默认内容是以下

由于结果是空,进入下一行关键方法 cacheEntry.validateResource

进入 StandardRootgetResourceInternal

进入 DirResourceSetgetResource

0x04 AbstractFileResourceSet file

进入核心方法 this.file 中 (AbstractFileResourceSet)

进入 JRE 方法 WinNTFileSystem#getCanonicalPath

该方法内容简单总结:将给定的文件路径标准化和规范化

可以看到在 JRE 中非常注重提升性能,避免重复代码和计算问题

关键部分我已经注释在代码中

观察到 cacheprefixCache 内容分别如下:一个是具体文件,一个是目录,都是 JRE 为性能而做的优化

注意到一个 native 方法 canonicalize0 底层做了什么

我找到了 WinNTFileSystem#canonicalize0C 代码

核心方法是:wcanonicalize

找到 jdkjdk/src/windows/native/java/io/canonicalize_md.c 方法

其中 FindFirstFileW 函数说明如下

https://learn.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-findfirstfilew

忽略了大部分代码,只保留关键代码:

总结:

为了验证结果,我本地 PUT 写了个 a.JSP 文件,手动访问 a.jsp 成功断点并截图,证明了我的猜测

下一步 a.jsp 小写 jsp 被写进去了缓存中 (a.jsp -> a.JSP)

注意高版本 Java 的该属性默认是 false 不存在缓存机制,你无法进入这个 else 分支

回到 AbstractFileResourceSet 的 file 方法后置处理部分

0x05 abs path 与 can path

getCanonicalPath 的结果是 can path

后续和 abs path 做比较,决定了返回是 null 还是具体的 File 对象

其中 abs path 是用户输入的路径拼接处理后的本地绝对路径(不一定必须存在)

其中 can path 是 JRE 类 WinNTFileSystem JNI/cache 处理后得到的路径

大体上分两种情况分析:

(1)PUT a.JSP 情况

abs path 显然是 /a.JSP

情况1.1 can path 无缓存,本地不存在 /a.JSP 文件,原样返回,得到 /a.JSP

情况1.2 can path 无缓存,本地存在 /a.JSP 文件,原样返回,得到 /a.JSP

情况1.3 can path 有缓存 a.JSP -> a.JSP 直接读缓存,原样返回,得到 /a.JSP

情况1.4 can path 有缓存 a.JSP -> a.JSP 直接读缓存,原样返回,得到 /a.JSP

以上任意一种情况,都一定会导致缓存中写入一条新的 a.JSP -> a.JSP

当我们访问 GET a.JSP 时,可以找到上传的 a.JSP 文件(此时由 DefaultServlet 处理静态资源)

(2)GET a.jsp 情况

abs path 显然是 /a.jsp

此时应该有三种缓存情况:

2.1. 缓存 a.jsp -> a.jsp 2.2. 缓存 a.jsp -> a.JSP 2.3. 没有缓存

情况2.1的原因:GET a.jsp 的时候没有上传 a.JSP 文件,原样返回了

情况2.2的原因:GET a.jsp 的时候已经存在 PUT 上传的 a.JSP 文件,返回以存在文件优先(参考上文 JNI 部分)

情况2.3的原因:高版本 JDK 默认不开缓存 / 手动关了缓存功能 / 第一次访问

情况2.1分析:can path 直接读缓存 /a.jsp,进入 JspServlet 后续处理阶段,能不能 RCE 看 a.JSP 文件在不在

情况2.2分析:can path 直接读缓存 /a.JSP,无法通过 equals 验证,返回空,最终返回 404

情况2.3分析:can path 返回看本地有没有 a.JSP(又分为以下两种情况)

情况2.3.1分析:本地有 a.JSP 返回 a.JSP 导致 equals 校验失败,给缓存添加了 a.jsp -> a.JSP

情况2.3.2分析:本地无 a.JSP 返回 a.jsp 可以过校验,给缓存添加了 a.jsp -> a.jsp

 

如果是 情况 2.3.2 第一次访问,缓存添加了 a.jsp -> a.jsp 内容

然后 PUT a.JSP 成功上传一个问题

再走 情况2.1 进入 JspServlet 后续处理阶段,因为 a.JSP 文件存在,导致了 RCE

 

0x06 二次校验

在我后续调试中,发现即使 a.jsp 的 equals 校验通过,后续还有一层校验

当顺利通过这一层之后,后续存在 Tomcat 内置缓存的处理

对得到的 resource 再次校验

跟入到达:this.root.getResourceInternal(this.webAppPath, useClassLoaderResources);

层层深入再次到达:AbstractFileResourceSet file 方法

我们要保证二次校验这里还是 a.jsp -> a.jsp

 

0x07 n次校验

还没有结束,在 JspServlet 后续处理的过程中,会再次抵达 AbstractFileResourceSet file 方法

JspCompilationContext#getLastModified

JspCompilationContext#getResource

 

还没有结束

JspCompilationContext#getLastModified 中调用了

到达 org.apache.catalina.webresources.CachedResource#getResource

再次抵达 AbstractFileResourceSet file 方法

 

最后达到真正的 JspServlet 读取文件的地方

JspUtil#getInputStream

再次抵达 AbstractFileResourceSet file 方法

 

0x08 竞争的是什么

竞争的难点是什么:我们需要保证,以上的 n 次 file 方法,取到的 cache 都必须是 a.jsp -> a.jsp

只要有一次是:a.jsp -> a.JSP 就会导致无法通过 equals 校验,返回 null 无法后续走完全程

回顾一下,什么情况下会出现:a.jsp -> a.JSP

PUT a.JSP 只要完成了,本地存在了 a.JSP 文件

就会导致下一次的 AbstractFileResourceSet file 方法中 can path 得到 a.JSP 和 abs path 不一致

 

以上,可以看到,保存了一次 a.jsp -> a.jsp

我们必须要 PUT a.JSP 文件落地之前

完美地通过以上 n 次的 AbstractFileResourceSet file 方法(注意这个 n 不止4次,实际需要更多次)

以上 n 次的 equals 校验,必须都拿到 a.jsp - > a.jsp 才会成功执行 jsp 文件导致 RCE

通过以上 n 次之后,还需要再 InputStream 读取的时候,文件正好落地,成功读到文件内容

 

0x09 过期策略

如果你仔细调试,你会发现为什么有时候 cache 里的 a.jsp -> a.jsp 不见了

因为 cache 有过期策略

所以我们的调试要在 30 秒内速速操作,超过就找不到了

 

0x0a 修复

CVE-2024-50379 的修复如下

https://github.com/apache/tomcat/commit/43b507ebac9d268b1ea3d908e296cc6e46795c00

https://github.com/apache/tomcat/commit/631500b0c9b2a2a2abb707e3de2e10a5936e5d41

读操作和写操作加了锁

官方修复时的想法应该是:(不考虑缓存情况)

当 PUT 操作没有完成写入的时候,GET a.jsp getResource 操作阻塞

官方的锁考虑了大小写,PUT a.JSP 的 write 操作和 GET a.jsp 的 read 操作是同一个锁,读和写冲突

 

CVE-2024-56337 二次修复

官方给出建议:必须设置该属性为 false

回顾上文 canonicalize 方法当该属性是 false 时直接 JNI 获取路径,不进行缓存

缓存这里存在的安全问题

如果某一个时刻

如果不使用缓存,将会出现以下问题:

没有缓存,于是当 PUT a.JSP 文件落地后,后续的 GET a.jsp 将全部得到 a.JSP 无法通过 equals 校验

正是因为缓存,导致了一种可能:PUT a.JSP 后可能 GET a.jsp 得到的 can path 还是 a.jsp

 

以上内容,考虑情况比较多且比较复杂,难免存在一些错误之处,欢迎师傅们讨论和指出问题

 

0x0b 检测

作为扫描器,不可以使用线程对冲的方式,可能会导致巨量脏数据

经过一些测试和综合考虑,我找到一种几乎无误报漏报,无损无脏数据的扫描方式

我已经给 xray 商业版(洞鉴)添加了该扫描方案,具体逻辑如下:

(1)判断 PUT 操作有效

如果 PUT 任意一个随机文件得到 204 返回,证明开启了 PUT 操作

补充:为了证实 PUT 操作确实有效和脏数据考虑,下一步可以 GET 访问确认成功,最后 DELETE 删除该临时文件

(2)判断操作系统

感谢漏洞百出群友的帮助,找到了一种判断目标是否是 WINDOWS 系统的方法

以 Linux 系统为例

PUT 上传一个 :.txt 文件成功,响应 201

访问 :.txt 成功,内容正确

但是在 Windows 系统会遇到 409 的报错

根据该差异,可以确定目标系统的类型

(3)版本信息

通过以上两点,可以证明开启 PUT 且是 WINDOWS

只需要再通过一个 404 路径,使用正则取到 Tomcat 报错版本信息

根据官方公告,精确匹配 tomcat 版本

通过这三步,即可完成一个几乎无误报漏报无损无脏数据的漏洞检测方案

(注意:几乎完美,但不能覆盖所有情况,如果用户手动关闭缓存,远程无法检测到)

 

0x0c 致谢

感谢 漏洞百出 群友一起交流以及给出 windows 系统检测思路

 

0x0d 缓存变动

时间匆忙,有些分析可能有点问题,再做两个补充

什么时候 a.jsp -> a.jsp 的缓存会被改成 a.jsp -> a.JSP

例如缓存超时这里的 30 秒,超过 30s 导致 a.jsp -> a.jsp 清空

如果 PUT a.JSP 成功,重新 can0 native 方法后修改缓存 a.jsp -> a.JSP 后续无法完成

总之只要 PUT a.JSP 文件落地,就会产生 a.jsp -> a.JSP 的可能性

 

0x0e 快速复现

快速复现漏洞的方式

前提条件,必须是 JAVA 8 且本地没有 a.jsp / a.JSP 文件(参考上文高版本默认不开缓存)

  1. GET a.jsp 使缓存保存一份 a.jsp -> a.jsp

  2. PUT a.JSP 使文件落地

  3. 由于有缓存,后续 file 方法都可以取到 a.jsp -> a.jsp

  4. 猛 GET a.jsp 即可触发漏洞(注意只在某个时间范围内可以成功触发)

  5. 超过某个时间点之后 a.jsp -> a.jsp 会变成 a.jsp -> a.JSP 导致后续无法触发