Hessian反序列化流程及漏洞浅析
多*瓜 漏洞分析 355浏览 · 2025-03-31 14:46

前言

Hessian是一个基于RPC的高性能二进制远程传输协议。 在Java中,Hessian的使用方法非常简单,它使用Java语言接口定义了远程对象,并通过序列化和反序列化将对象转为Hessian二进制格式进行传输。

项目依赖:

反序列化流程分析

序列化

HessianOutputHessian2Output都是抽象类AbstractHessianOutput的实现

二者的writeObject方法一致: 根据传入的object的类型,获取对应需要的序列化器 然后调用序列化器的writeObject方法序列化数据。

调用com.caucho.hessian.io.SerializerFactory#getSerializer方法获取对应序列化器。 先判断_cachedSerializerMap中是否有缓存,如果有直接取出。 没有缓存就调用com.caucho.hessian.io.SerializerFactory#loadSerializer方法进行加载序列化器; 最后将得到的序列化器存储到缓存的map中。


com.caucho.hessian.io.SerializerFactory#loadSerializer方法中。 判断当前传入的Object是否属于某些已定义好的接口。 如果存在,就生成对应的序列化器。 如果不存在,就调用com.caucho.hessian.io.SerializerFactory#getDefaultSerializer方法针对自定义类加载默认的序列化器。
共实现了26个序列化器


com.caucho.hessian.io.SerializerFactory#getDefaultSerializer方法中,可以看到在默认情况下如果_isEnableUnsafeSerializer属性为true,并且传入的class: ·cl没有writeReplace方法, 那么最后会创造一个UnsafeSerializer来作为序列化器。


UnsafeSerializer#writeObject方法兼容了 Hessian/Hessian2 两种协议的数据结构,会调用writeObjectBegin 方法开始写入数据头,并且根据返回的ref来确定后续序列化数据的情况。


HessianOutput,会直接调用父类的com.caucho.hessian.io.AbstractHessianOutput#writeObjectBegin方法,可以看到直接写入77作为Map的标志,固定返回-2赋值给writeObject方法
之后就调用com.caucho.hessian.io.UnsafeSerializer#writeObject10方法,来逐个对字段进行序列化。并已writeMapEnd作为收尾。


Hessian2Output重写了writeObjectBegin方法,可以写自定义类型的数据,返回ref为-1。调用 writeDefinition20 和 Hessian2Output#writeObjectBegin 方法写入自定义数据,不将其标记为 Map 类型。


小结: 总的来说: 二者在序列化自定义类的过程中均使用UnsafeSerializer序列化器,Hessian1Hessian2协议在处理首字段的时候,有了细微的差异。

HessianOutput 在序列化的过程中默认将序列化结果处理成一个Map

Hessian2Output在序列化的过程中可以序列化自定义的类

反序列化

HessianInputHessian2Input都是抽象类AbstractHessianInput的实现类

Hessian1

com.caucho.hessian.io.HessianInput#readObject()方法中读取序列化结果的第一个字符为77,即代表map


跟进com.caucho.hessian.io.SerializerFactory#readMap方法
先调用com.caucho.hessian.io.SerializerFactory#getDeserializer(java.lang.String)方法, 首先如果传入的type为空,则直接返回null 接着再判断缓存中是否有对应的序列化器, 如果没有就尝试自己去加载获取序列化器。


这里由于是最外层封装的map,获取的type'',默认返回回到com.caucho.hessian.io.SerializerFactory#readMap方法,直接初始化一个MapDeserializer实例类,从而调用com.caucho.hessian.io.MapDeserializer#readMap方法来反序列化内部的数据。

如果是内部其他类型的类,首先调用com.caucho.hessian.io.SerializerFactory#loadSerializedClass方法,根据类名加载对应的类。
接着调用com.caucho.hessian.io.SerializerFactory#getDeserializer(java.lang.Class)方法,来获取对应的序列化器。


接着com.caucho.hessian.io.SerializerFactory#loadDeserializer方法,
在该方法中加载默认的自定义类。可以看到与序列化过程中获取加载器的流程相近,不再赘述。 com.caucho.hessian.io.SerializerFactory#getDefaultDeserializer


Hessian2

这里以我们自定义类Person反序列化为例,首先在com.caucho.hessian.io.Hessian2Input#readObject()方法中,获取对应的tag67,因此调用com.caucho.hessian.io.Hessian2Input#readObjectDefinition方法


会进一步调用com.caucho.hessian.io.SerializerFactory#getObjectDeserializer(java.lang.String, java.lang.Class)方法来获取对应的序列化器。
一步一步步入,发现会执行到com.caucho.hessian.io.SerializerFactory#getDeserializer(java.lang.String)方法,这里与序列化过程中获取序列化器过程一样,最终会获取到一个UnsafeDeserializer序列化器。


回到readObjectDefinition方法, 获取自定义类的相关属性,并将其封装为def属性。


最后会调用com.caucho.hessian.io.UnsafeDeserializer#readObject方法,将封装好的字段通过unSafe进行反射赋值。


instantiate 使用 unsafe 实例的 allocateInstance 直接创建类实例。


MapDeserializer

Hessian 1.0 默认最外层会使用MapDeserializer来继续反序列化数据。 Hessian 2.0 需要指定传入的类的类型为Map, 才会使用MapDeserializer来反序列化数据

com.caucho.hessian.io.MapDeserializer#readMap的方法中: 创建得到一个map类型,之后通过一个循环判断 in.isEnd() 方法检查输入流是否结束。 在循环中,通过 in.readObject() 方法读取键值对,并通过map.put进行赋值。这里调用的还是HessianInputreadObject方法。 最后调用in.readEnd结束map的反序列化赋值。



显然map.put

对于HashMap会触发key.hashCode()key.equals(k)

对于TreeMap会触发key.compareTo()

漏洞分析

Hessian反序列化Map类型的对象的时候,会自动调用其put方法,而put方法会产生各种相关利用链打法。

典型就是Rome的相关链子,通过HashMapkey会触发hash方法,会进一步触发key.hashcode


触发EqualsBeanhashcode方法


接着触发toStringBeantoString方法, 会反射调用该类所有的无参get方法。从而实现漏洞利用


TemplatesImpl 失败原因&&二次反序列化

单独打TemplatesImpl 失败原因分析

根据Rome的调用链,有如下POC

代码执行后,无命令回显。 Debug分析,来到com.sun.syndication.feed.impl.ToStringBean#toString(java.lang.String)方法,发现存在报错空指针。
跟踪报错栈帧,往上看,报错位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses方法中,发现此时的_tfactory没有被反序列化赋值,为null,从而报错空指针。



这是因为在通过UnsafeDeserializer序列化器调用getFieldMap方法的时候,会对类的属性判断是否为Transient类型,是否为static类型。 如果是transient或者static类型的变量,则无法进行反序列化。


这里赋值的_tfactory恰好为transient类型所修饰,因此无法被反序列化。


二次反序列化打TemplatesImpl

这里SignedObject类利用内部的content变量可以存储原生序列化的字节流,从而可以保存TemplatesImpl恶意类的相关属性。

SignedObject类常被用来作为二次反序列化。 它的构造函数中将传入的object类通过原生序列化转化为字节流存储到content变量中。 并且它的getObject方法中又会对content属性进行原生的反序列化。


并且SignedObjectgetObject方法也满足ToStringBean#toString方法,也满足Rome链的使用情况,因此可以用来打二次反序列化。

给定如下POC:



JdbcRowSetImpl链

回顾一下JdbcRowSetImpl链, getParameterMetaData方法中,会调用connect方法
connect方法中,会对传入的dataSourceName值进行lookup查询,触发JNDI注入


显然,由于存在getDataBaseMetaData的无参get方法,显然可以用于触发ToStringBeantoString方法,因此有如下payload

小debug: 值得注意的是我在payload中添加了jdbcRowSet.setMatchColumn("reus09");语句。 由于我本地测试环境为arm架构的mac,因此使用的jdk版本较高,需要手动设置trustURLCodebase的相关属性。 并且JdbcRowSetImpl类的相关属性获取也存在问题。 我们需要的getDatabaseMetaData是第五个获取, 但是在获取第四个字段matchColumnNames的时候,反射执行getMatchColumnNames方法会产生空指针报错,导致反射获取我们需要的方法无法执行。




分析getMatchColumnNames方法,我们需要对strMatchColumns属性进行赋值,否则就会报错。


找到setMatchColumn方法,传入任意的字符,即可对strMatchColumns属性进行赋值。


因此,在高版本的JDK中,针对JdbcRowSetImplRome链结合使用,在学习的过程中,可能需要稍微对字段继续优化、debug一下。

小结

本文分析了Hessian以及Hessian2两种序列化和反序列化的流程。 总的来说,Hessian会针对传入的map类型的变量进行反序列化的时候,会执行map.put方法,从而可以作为source触发点,触发其他相关的反序列链子。 并以二次反序列化和JdbcRowSetImpl两个链作为例子进行了演示。

Reference

Hessian 反序列化知一二 素十八

从源码角度分析hessian特别的原因

2022虎符CTF-Java部分

Java安全学习——Hessian反序列化漏洞 - 枫のBlog

【Web】浅聊Java反序列化之玩转Hessian反序列化的前置知识hessian 反序列化-CSDN博客

1 条评论
某人
表情
可输入 255