Tomcat的SESSIONS.ser文件利用及其源码分析
介绍
Tomcat
默认会开启 session
的持久化功能,它是通过当 Tomcat
正常退出的时候,会将 session
的值进行序列化,存到 work
目录下的 SESSIONS.ser
文件中(正常来说完整路径是 <Tomcat的安装目录>/work/Catalina/localhost/<项目名>/SESSSIONS.ser
),然后下次启动时会对这个文件中的内容进行反序列化(启动成功后就会删除 SESSIONS.ser
文件),从而恢复 session
的值。
如果我们可以控制 SESSIONS.ser
文件的内容(往往是通过任意文件上传),就可能造成反序列化漏洞。
源码分析
这里先来分析一下 SESSIONS.ser
文件的内容。先来试着写入值到 session
中,然后关闭 Tomcat
。
1 | package com.just.servlet; |
然后可以得到上面的 session
对应的 SESSIONS.ser
文件。
根据开头的序列化魔数 ACED0005
,就可以很明显的知道里面存的是 jdk
原生序列化的内容。至于其内容存的信息,我们可以分析其源码。
我们可以找到持久化 session
的源码在 org.apache.catalina.session.StandardSession
类中的 doReadObject()
和 doWriteObject()
方法中。
根据这里的源码,我们就可以知道其序列化存的信息是什么了。但是这里需要注意的是,如果熟悉序列化的结构的人,会发现这里貌似源码和 SESSIONS.ser
文件的内容对不上。因为 SESSIONS.ser
文件中的第一块序列化的数据很明显是 java.lang.Integer
类型的,而源码中第一次 readObject()
返回的结果显示是 Long
类型的。很明显在 doReadObject()
之前还有一次反序列化。然后不难找到,在这里传入 stream
给 doReadObject()
方法的时候其实已经操作过 stream
了。
这下就对的上了。
根据分析源码,不难得到这里 SESSIONS.ser
的结构是:
开头一个数字标识有几个
session
,然后后面的就是各个session
的具体信息。
有一点需要注意的是的是:
我们可以发现这里都是调用的
readObject()
然后对结果进行强制类型转化,而非调用readInt()
,readDouble()
方法。这样我们构造恶意的SESSIONS.ser
文件就很容易了,就不需要在意其结构,就直接写入一个恶意的对象。因为对readObject()
结果进行强制类型转化并不会影响反序列化漏洞,而调用的不是readObject()
而是其它readXxx()
方法,就需要考虑写入SESSIONS.ser
文件的结构才能实现反序列化漏洞。
不过再怎么样也都可以造成反序列化漏洞,这里只是说明一下为什么直接序列化一个恶意的对象到SESSIONS.ser
文件中就可以造成反序列化漏洞了。
然后我们分析一下 是怎么调用的 doReadObject()
和 doWriteObject()
方法。
我们可以调试 Tomcat
启动的过程。发现其核心关于持久化 session
的逻辑在 org.apache.catalina.session.StandardManager
中。其通过 doLoad()
方法在 Tomcat
停止的时候加载 SESSIONS.ser
文件然后删除 SESSIONS.ser
文件,通过 doUnload()
方法在 Tomcat
停止的时候存储 session
的内容到 SESSION.ser
文件中。
1 | protected void doLoad() throws ClassNotFoundException, IOException { |
并且可以发现其存储的路径是通过下面的代码获取的:
这里的 pathname
默认值就是 SESSIONS.ser
。
实验测试
我们写一个 cc9
的反序列化 exp
到 SESSIONS.ser
文件中,然后启动 Tomcat
,发现成功命令执行。
进一步分析
根据上面的分析,我们可以发现这种攻击方式其实很鸡肋,因为不仅需要程序项目没有用到 session
(因为如果项目自身用到了 session
,那么在关闭 Tomcat
的时候写入 session
的内容到 SESSIONS.ser
文件中,就会覆盖我们上传的 SESSIONS.ser
文件,但是这点并不是很鸡肋,因为现在大部分的项目都是前后端分离,使用的是 token
鉴权而非 session
,所以现在很多项目都没有用 session
),最重要是需要重新启动 Tomcat
才能生效,而实战中我们几乎不可能让目标重启 Tomcat
。
有没有办法让目标不重启就可以加载 SESSIONS.ser
文件呢?
答案是可以的!
Whenever Apache Tomcat is shut down normally and restarted, or when an application reload is triggered, the standard Manager implementation will attempt to serialize all currently active sessions to a disk file located via the pathname attribute. All such saved sessions will then be deserialized and activated (assuming they have not expired in the mean time) when the application reload is completed.
因此根据官方文档可以看出来,除了服务停止或者重启,还可以让部署的程序触发 reload
来做到。
这里的思路是参考
2022 rwctf Desperate Cat
的出题人wp
的,其中wp
利用到了这个技巧。
让 Tomcat
部署的程序进行 reload
有两种方式:
第一种reload的方式
第一种 reload
的方式需要满足两个条件:
Context reloadable
配置为true
(默认是false
);/WEB-INF/classes/
或者/WEB-INF/lib/
目录下的文件发生变化。
由于 Context reloadable
默认是 false
,要动态修改它可以通过执行:
1 | ${pageContext.servletContext.classLoader.resources.context.reloadable=true} |
至于怎么修改,就具体情况具体分析了。
然后上传文件到 /WEB-INF/classes/
目录或者 /WEB-INF/lib/
目录下就可以触发 Tomcat
reload
程序了。
但是这里需要注意的是:如果上传的
jar
包或者class
文件的格式错误,会导致程序异常崩溃,从而整个网页都无法正常访问了。
第二种reload的方式
第一种方式由于需要修改 reloadable
的值,但是大部分情况应该都是改不了的,所以相对来说不是很好用。这里可以用第二种方式。
WatchedResource - The auto deployer will monitor the specified static resource of the web application for updates, and will reload the web application if it is updated. The content of this element must be a string.
在 Tomcat 9
环境下,默认的 WatchedResource
包括:
WEB-INF/web.xml
WEB-INF/tomcat-web.xml
${CATALINA_HOME}/conf/web.xml
Tomcat
会有后台线程去监控这些文件资源,在 Tomcat
开启 autoDeploy
的情况下(此值默认为 true
,即默认开启 autoDeploy
),一旦发现这些文件资源的 lastModified
时间被修改,也会触发 reload
:
由于应用本身没有 WEB-INF/tomcat-web.xml
配置文件, 因此通过利用程序本身的写文件漏洞,来创建一个 WEB-INF/tomcat-web.xml/
目录,也可以让应用强行触发 reload
,加载并反序列化先前写入的恶意 SESSIONS.ser
文件。
总结
当 Tomcat
部署的程序具有任意文件上传漏洞的时候,我们可以先上传一个恶意的 SESSIONS.ser
文件到 Tomcat
的 work
目录,然后再上传一个 WEB-INF/tomcat-web.xml
文件(内容随便)来触发 Tomcat
的 reload
,进而反序列化 SESSIONS.ser
文件产生反序列化漏洞。
此外,如果靶机可以加载我们指定任意的类,我们还可以上传恶意的 jar
包到 WEB-INF/lib/
目录下,然后再来指定靶机加载我们恶意的 jar
包中的 class
文件,执行其中 static
代码块中的代码,造成 RCE
。这种方式好在不需要靶机有反序列化漏洞的组件,但是需要我们能够加载我们通过文件上传漏洞上传的 jar
包中的 class
文件。