好几周前了吧,Apache Tomcat发布通告称修复了一个源于持久化Session的远程代码执行漏洞(CVE-2020-9484)。影响<= 9.0.34,<= 8.5.54,<= 7.0.103版本。(其他描述就不搬运了)
但据说漏洞利用成功的条件比较苛刻。
看到有很多linux下复现的,这里就来试试Windows下的,亲手看看到底有多苛刻。
0x01复现
首先还是漏洞环境的搭建。这里是apache-tomcat-8.5.46和windows环境。
需要的条件1:
在tomcat的…\apache-tomcat-8.5.46\conf\content.xml配置文件下需要在节点下添加以下配置。
<Manager className="org.apache.catalina.session.PersistentManager"
debug="0"
saveOnRestart="false"
maxActiveSession="-1"
minIdleSwap="-1"
maxIdleSwap="-1"
maxIdleBackup="-1">
<Store className="org.apache.catalina.session.FileStore" directory="./session" />
</Manager>
总体配置完就是这样:
这个配置是有什么用呢?
通常来说Session 是保存在内存中的,如果服务器重启、宕机的话,Session 就会丢失。所以有时候,我们需要对Session 持久化以应对意外的情况发生。
还有一种情况,我们需要持久化 Session。如果当前用户的访问量巨大,大量的 Session 便会占用服务器大量的内存,从而使服务器的性能受到影响。如果能将一些闲置时间较长的 Session 换出,存储至磁盘,便可以起到节省内存空间的作用。
但要持久化 Session,那么 Session 里存放的对象必须是可序列化的,即实现了 java.io.Serializable 接口。(这里就有了隐患)
这里的一种实现方式就是使用PersistentManager。
PersistentManager 通过使用 Store 将内存中的 session 拷贝至文件或数据库中。如果当前活动的 session 对象数量超过了上限值或者 session 对象闲置了过长时间,就会有 session 对象就会被换出,存储到磁盘中,以节省内存空间。
当Tomcat正常关闭、重启或Web应用程序重新加载时,PersistentManager 也会像 StandardManager 一样,将 session 对象持久化到磁盘中。当 session 对象复制存储至磁盘中,原 session 对象可能仍存留在内存中。
因此,如果Web应用突然非正常终止或服务器崩溃了,当服务器重启,Web应用重新加载的时候,便可以从磁盘中还原已持久化的session。
这里的Store 有两种:
FileStore 和 JDBCStore,分别用作于将 session 存储至文件和数据库。(这里猜想一下如果能控制数据库中的内容大概也可以通过JDBCStore实现)
这个漏洞利用的就是FileStore这种方式:
FileStore 用作于将 session 存储至文件,通过 元素的 directory 属性指定文件所在的目录,具体就可参考上述的配置
<Store className="org.apache.catalina.session.FileStore" directory="./session" />
在上述的配置中,session 对象就会被存储至 $CATALINA_HOME/work/Catalina/hostname/webappname/session/sessionID.session 文件中。
需要的条件2:
前面提到了序列化的对象,所以这里就需要在已有的文件目录下有已经存在序列化过的恶意文件,通常可以配合任意文件上传实现。
所以这里我们在…\apache-tomcat-8.5.46\temp\目录下手动生成了一个test.session的反序列化的payload。
这里可根据目标环境去自行生成合适的,比如我这里的demo使用了commons-collections4-4.0,我就使用了ysoserial的CommonsCollections2。
路径的话,默认的配置是刚才的xml中配置的,但是由于这里的路径可以自己去构造。所以理论是可以放在任何路径下。
所以我们构造payload
即可看到成功触发
0x02分析
由于前面说到了是使用org.apache.catalina.session.FileStore处理,这里就首先看下这个类。
由org.apache.catalina.session.FileStore#load这个方法可看到String id就是给出的具体路径。
然后通过id来获取到相关文件。接着根据给出的路径读取相应的序列化文件然后调用readObjectData来进行反序列化。
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) {
return null;
}
if (!file.exists()) {
return null;
}
Context context = getManager().getContext();
Log contextLog = context.getLogger();
if (contextLog.isDebugEnabled()) {
contextLog.debug(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("No persisted data file found");
}
return null;
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldThreadContextCL);
}
}
通过调试可以发现,这里传入的id就是我们所构造的序列化文件的路径
file的值就会变成,上文所说的配置文件的路径和我们传入的JSESSIONID的路径拼接的结果。我们可以看到在这里是直接拼接的,这也是为什么我们的序列化payload文件可放在任意目录下。
最终我们读入的序列化对象调用了readObjectData完成了反序列化
为什么readObjectData可以完成反序列化,跟进之后可以看到实际调用了doReadObject
然而doReadObject里,就又调用了readObject。那么这就原因所在了。
0x03 小结
由于以上种种条件限制,还得要知道FileStore使用的存储位置到可控文件的相对路径,可以发现我们这个要想成功利用确实有些难度。
单满足一种在实战中都是小概率事件,同时满足几乎是很难遇到,但真不排除有撞大运的可能。或者在一些特殊的条件下也许可以变相去利用。
而且,即使很鸡肋,作为一个学习的案例,不也很好嘛!
当然如有不正确之处还请大家多多交流指正。
----END----
参考:
------------- 期待您的进一步交流-------------