Hessian反序列化流程及漏洞分析
前言
Hessian是一个基于RPC的高性能二进制远程传输协议。在Java中,Hessian的使用方法非常简单,它使用Java语言接口定义了远程对象,并通过序列化和反序列化将对象转为Hessian二进制格式进行传输。
项目依赖:
<!-- hessian -->
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
反序列化流程分析
序列化
HessianOutput和Hessian2Output都是抽象类AbstractHessianOutput的实现,二者的writeObject方法一致:
public void writeObject(Object object) throws IOException {
if (object == null) {
this.writeNull();
} else {
Serializer serializer = this._serializerFactory.getSerializer(object.getClass());
serializer.writeObject(object, this);
}
}
-
调用
com.caucho.hessian.io.SerializerFactory#getSerializer方法获取对应序列化器:- 先判断
_cachedSerializerMap中是否有缓存,如果有直接取出 - 没有缓存就调用
com.caucho.hessian.io.SerializerFactory#loadSerializer方法进行加载序列化器 - 最后将得到的序列化器存储到缓存的map中
- 先判断
-
在
loadSerializer方法中:- 判断当前传入的Object是否属于某些已定义好的接口
- 如果存在,就生成对应的序列化器
- 如果不存在,就调用
getDefaultSerializer方法针对自定义类加载默认的序列化器
-
在
getDefaultSerializer方法中:- 如果
_isEnableUnsafeSerializer属性为true,并且传入的class没有writeReplace方法 - 那么会创造一个UnsafeSerializer来作为序列化器
- 如果
-
UnsafeSerializer#writeObject方法:- 兼容Hessian/Hessian2两种协议的数据结构
- 调用
writeObjectBegin方法开始写入数据头 - 根据返回的ref来确定后续序列化数据的情况
Hessian1和Hessian2的区别:
- HessianOutput会直接调用父类的
writeObjectBegin方法,直接写入77作为Map的标志,固定返回-2 - Hessian2Output重写了
writeObjectBegin方法,可以写自定义类型的数据,返回ref为-1
小结:
- 二者在序列化自定义类的过程中均使用UnsafeSerializer序列化器
- Hessian1默认将序列化结果处理成一个Map
- Hessian2可以序列化自定义的类
反序列化
HessianInput和Hessian2Input都是抽象类AbstractHessianInput的实现类。
Hessian1
-
HessianInput#readObject()方法中读取序列化结果的第一个字符为77,即代表map -
调用
SerializerFactory#readMap方法:- 先调用
getDeserializer(String)方法获取反序列化器 - 由于是最外层封装的map,获取的type为空,默认返回null
- 直接初始化一个MapDeserializer实例类
- 调用
MapDeserializer#readMap方法来反序列化内部的数据
- 先调用
-
对于内部其他类型的类:
- 调用
loadSerializedClass方法根据类名加载对应的类 - 调用
getDeserializer(Class)方法获取对应的序列化器 - 调用
loadDeserializer方法加载默认的自定义类
- 调用
Hessian2
-
以自定义类Person反序列化为例:
Hessian2Input#readObject()方法中获取对应的tag为67- 调用
readObjectDefinition方法 - 调用
getObjectDeserializer方法获取序列化器 - 最终获取到一个UnsafeDeserializer序列化器
-
readObjectDefinition方法:- 获取自定义类的相关属性
- 将其封装为def属性
-
UnsafeDeserializer#readObject方法:- 将封装好的字段通过unSafe进行反射赋值
instantiate使用unsafe的allocateInstance直接创建类实例
MapDeserializer
- Hessian 1.0默认最外层会使用MapDeserializer来继续反序列化数据
- Hessian 2.0需要指定传入的类的类型为Map,才会使用MapDeserializer来反序列化数据
MapDeserializer#readMap方法:
- 创建一个map类型
- 通过循环判断
in.isEnd()检查输入流是否结束 - 在循环中,通过
in.readObject()方法读取键值对,并通过map.put进行赋值 - 调用
in.readEnd结束map的反序列化赋值
注意:
- 对于HashMap会触发
key.hashCode()、key.equals(k) - 对于TreeMap会触发
key.compareTo()
漏洞分析
Hessian反序列化Map类型的对象的时候,会自动调用其put方法,而put方法会产生各种相关利用链打法。
Rome链利用
典型利用是通过HashMap中key会触发hash方法,进而触发key.hashcode():
- 触发EqualsBean的hashcode方法
- 触发toStringBean的toString方法
- toString方法会反射调用该类所有的无参get方法,从而实现漏洞利用
TemplatesImpl失败原因分析
单独打TemplatesImpl失败原因:
- 在
ToStringBean#toString方法中,TemplatesImpl#defineTransletClasses方法报错空指针 _tfactory没有被反序列化赋值,为null- 原因:
UnsafeDeserializer序列化器会判断类的属性是否为Transient或static类型 _tfactory恰好为transient类型修饰,无法被反序列化
二次反序列化打TemplatesImpl
使用SignedObject类进行二次反序列化:
- SignedObject内部content变量可以存储原生序列化的字节流
- 构造函数中将传入的object类通过原生序列化转化为字节流存储到content变量
- getObject方法又会对content属性进行原生的反序列化
- SignedObject的getObject方法也满足ToStringBean#toString方法,满足Rome链的使用情况
JdbcRowSetImpl链
JdbcRowSetImpl链分析:
- 在
getParameterMetaData方法中会调用connect方法 - connect方法会对传入的dataSourceName值进行lookup查询,触发JNDI注入
- 由于存在getDataBaseMetaData的无参get方法,可以用于触发ToStringBean的toString方法
高版本JDK注意事项:
- 需要手动设置trustURLCodebase的相关属性
- JdbcRowSetImpl类的相关属性获取存在问题
- 需要先调用
setMatchColumn方法对strMatchColumns属性进行赋值,避免空指针报错
小结
- 分析了Hessian以及Hessian2两种序列化和反序列化的流程
- Hessian会针对传入的map类型的变量进行反序列化时执行map.put方法,可作为source触发点
- 演示了二次反序列化和JdbcRowSetImpl两个利用链
Reference
- Hessian 反序列化知一二 - 素十八
- 从源码角度分析hessian特别的原因
- 2022虎符CTF-Java部分
- Java安全学习——Hessian反序列化漏洞 - 枫のBlog
- 浅聊Java反序列化之玩转Hessian反序列化的前置知识
- hessian 反序列化-CSDN博客