Hessian反序列化流程及漏洞浅析
字数 3303 2025-08-30 06:50:36

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);
    }
}
  1. 调用com.caucho.hessian.io.SerializerFactory#getSerializer方法获取对应序列化器:

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

    • 判断当前传入的Object是否属于某些已定义好的接口
    • 如果存在,就生成对应的序列化器
    • 如果不存在,就调用getDefaultSerializer方法针对自定义类加载默认的序列化器
  3. getDefaultSerializer方法中:

    • 如果_isEnableUnsafeSerializer属性为true,并且传入的class没有writeReplace方法
    • 那么会创造一个UnsafeSerializer来作为序列化器
  4. 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

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

  2. 调用SerializerFactory#readMap方法:

    • 先调用getDeserializer(String)方法获取反序列化器
    • 由于是最外层封装的map,获取的type为空,默认返回null
    • 直接初始化一个MapDeserializer实例类
    • 调用MapDeserializer#readMap方法来反序列化内部的数据
  3. 对于内部其他类型的类:

    • 调用loadSerializedClass方法根据类名加载对应的类
    • 调用getDeserializer(Class)方法获取对应的序列化器
    • 调用loadDeserializer方法加载默认的自定义类

Hessian2

  1. 以自定义类Person反序列化为例:

    • Hessian2Input#readObject()方法中获取对应的tag为67
    • 调用readObjectDefinition方法
    • 调用getObjectDeserializer方法获取序列化器
    • 最终获取到一个UnsafeDeserializer序列化器
  2. readObjectDefinition方法:

    • 获取自定义类的相关属性
    • 将其封装为def属性
  3. UnsafeDeserializer#readObject方法:

    • 将封装好的字段通过unSafe进行反射赋值
    • instantiate使用unsafe的allocateInstance直接创建类实例

MapDeserializer

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

MapDeserializer#readMap方法:

  1. 创建一个map类型
  2. 通过循环判断in.isEnd()检查输入流是否结束
  3. 在循环中,通过in.readObject()方法读取键值对,并通过map.put进行赋值
  4. 调用in.readEnd结束map的反序列化赋值

注意

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

漏洞分析

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

Rome链利用

典型利用是通过HashMap中key会触发hash方法,进而触发key.hashcode():

  1. 触发EqualsBean的hashcode方法
  2. 触发toStringBean的toString方法
  3. toString方法会反射调用该类所有的无参get方法,从而实现漏洞利用

TemplatesImpl失败原因分析

单独打TemplatesImpl失败原因:

  1. ToStringBean#toString方法中,TemplatesImpl#defineTransletClasses方法报错空指针
  2. _tfactory没有被反序列化赋值,为null
  3. 原因:UnsafeDeserializer序列化器会判断类的属性是否为Transient或static类型
  4. _tfactory恰好为transient类型修饰,无法被反序列化

二次反序列化打TemplatesImpl

使用SignedObject类进行二次反序列化:

  1. SignedObject内部content变量可以存储原生序列化的字节流
  2. 构造函数中将传入的object类通过原生序列化转化为字节流存储到content变量
  3. getObject方法又会对content属性进行原生的反序列化
  4. SignedObject的getObject方法也满足ToStringBean#toString方法,满足Rome链的使用情况

JdbcRowSetImpl链

JdbcRowSetImpl链分析:

  1. getParameterMetaData方法中会调用connect方法
  2. connect方法会对传入的dataSourceName值进行lookup查询,触发JNDI注入
  3. 由于存在getDataBaseMetaData的无参get方法,可以用于触发ToStringBean的toString方法

高版本JDK注意事项

  1. 需要手动设置trustURLCodebase的相关属性
  2. JdbcRowSetImpl类的相关属性获取存在问题
  3. 需要先调用setMatchColumn方法对strMatchColumns属性进行赋值,避免空指针报错

小结

  1. 分析了Hessian以及Hessian2两种序列化和反序列化的流程
  2. Hessian会针对传入的map类型的变量进行反序列化时执行map.put方法,可作为source触发点
  3. 演示了二次反序列化和JdbcRowSetImpl两个利用链

Reference

  1. Hessian 反序列化知一二 - 素十八
  2. 从源码角度分析hessian特别的原因
  3. 2022虎符CTF-Java部分
  4. Java安全学习——Hessian反序列化漏洞 - 枫のBlog
  5. 浅聊Java反序列化之玩转Hessian反序列化的前置知识
  6. hessian 反序列化-CSDN博客
Hessian反序列化流程及漏洞分析 前言 Hessian是一个基于RPC的高性能二进制远程传输协议。在Java中,Hessian的使用方法非常简单,它使用Java语言接口定义了远程对象,并通过序列化和反序列化将对象转为Hessian二进制格式进行传输。 项目依赖: 反序列化流程分析 序列化 HessianOutput和Hessian2Output都是抽象类AbstractHessianOutput的实现,二者的writeObject方法一致: 调用 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博客