Hessian反序列化漏洞深度解析:流程、修复与利用链大全
字数 5933
更新时间 2026-03-07 12:11:20

Hessian 序列化与反序列化安全漏洞解析与利用链总览

本文档基于 阿里云先知社区文章 内容整理

前言

本文主要目标是:

  1. 整理Hessian 1/2的序列化与反序列化流程。
  2. 探讨Hessian反序列化漏洞的临时修复方案。
  3. 收集Hessian反序列化中的各种利用链(Gadgets)与绕过技巧。
  4. 补充其他Hessian相关的知识点。

第一章:序列化与反序列化流程

1.1 序列化过程

代码位置: GitHub示例代码库
环境说明: 序列化工厂设置允许序列化未实现Serializable接口的类。

1.1.1 HessianOutput

  • 入口: com.caucho.hessian.io.HessianOutput#writeObject
  • 流程:
    1. 获取序列化器: 判断序列化对象非空后,根据对象所属类,调用_serializerFactory.getSerializer获取序列化器。
    2. 序列化器缓存机制: SerializerFactory.getSerializer优先从缓存Map中查找。若无,则调用loadSerializer
    3. 加载序列化器 (loadSerializer):
      • 第295行:_contextFactory.getSerializer(cl.getName()) 根据类名直接获取39个基本类型及其包装类的序列化器。
      • 第309行:尝试获取自定义序列化器 (XXXXHessianSerializer)。
      • 经过一系列if判断特定类型后,未匹配的自定义类最终走到getDefaultSerializer
    4. 默认序列化器 (getDefaultSerializer):
      • 未设置默认序列化器时,判断类是否实现Serializable接口,或工厂设置了_isAllowNonSerializable(解除序列化限制的关键)。
      • 默认情况下,会在392行创建UnsafeSerializer
    5. UnsafeSerializer初始化 (createintrospect):
      • 通过反射“内省”获取类中所有公开属性和方法。
      • 第124行:属性若被transientstatic修饰,则不参与序列化。这是利用链中不能依赖transient/static变量的原因(如TemplatesImpl_tfactory)。
    6. 写入对象: 序列化器就绪后,调用writeObject。对于自定义对象:
      • writeObjectBegin写入固定字节Mt,表明Hessian1将自定义对象视为Map序列化,该方法始终返回-2
      • 进入writeObject10,依次写入每个属性的名称和值,最后写入Map结束符。
  • Map对象的序列化: 调用MapSerializer,写入类型为null,第一个字节为M,之后对每个键值对调用writeObject

1.1.2 Hessian2Output

  • 入口: com.caucho.hessian.io.Hessian2Output#writeObject
  • 核心差异:
    • 获取序列化器仍调用SerializerFactory.getSerializer
    • 类名引用机制: writeObjectBegin会根据类名是否首次出现而采取不同写入方式:
      • 首次出现:写入标记C + 完整的类名字符串。
      • 已出现过:写入一个整数引用ID以节省流量。
    • 序列化逻辑:先写入属性数量和属性名称列表,然后在writeInstance中再写入属性值。
  • Map对象的序列化: 仍调用MapSerializer,写入类型为null,第一个字节为H

1.2 反序列化过程

1.2.1 HessianInput

  • 入口: com.caucho.hessian.io.HessianInput#readObject()
  • 流程:
    1. 判断类型: 通过switch进入相应分支。自定义对象和Map对象写入的第一个字节都是M,故进入M对应的case
    2. 获取反序列化器: 调用SerializerFactory.readMap -> getDeserializer。其过程与getSerializer类似,最终获取UnsafeDeserializer
    3. UnsafeDeserializer初始化: 在构造方法中通过反射获取类属性,将属性名和对应的反序列化器放入fieldMap
    4. 实例化与属性还原: 调用instantiate,通过Unsafe直接实例化对象,然后调用readMap还原各个属性。反序列化器从fieldMap取出,通过Unsafe方法根据内存偏移量直接设置值。
  • HashMap的反序列化: 稍有不同。序列化时写入的typenull,最终调用MapDeserializer。其中map.put是关键利用入口,可触发key.hashCodekey.equalsHashMap)或compareToTreeMap)。

1.2.2 HessianInput2

  • 流程: 同样通过switch判断类型。
  • 关键差异: 先读取类名、属性数量并还原属性定义(不设值),将定义包装为ObjectDefinition存入_classDefs。随后在readObject中读取并设置属性值。对于自定义对象,不再默认解析为Map类型。

1.3 流程小结与利用条件

通过上述分析,得出Hessian反序列化利用的核心条件:

  1. 入口点(Source): 必须是hashCodeequalscompareTo方法。
  2. 属性限制: 利用链中不能有transientstatic修饰的变量。

第二章:临时修复方案

主要通过重写SerializerFactory,添加恶意类的黑名单过滤,阻止不安全的类被反序列化。

第三章:利用链总结

本章对各类利用链原理进行简要分析,并附上参考代码。

3.1 HessianProxyFactory (JNDI利用)

  • 来源: AliyunCTF 2026
  • 原理: 高版本JDK中,JNDI利用常依赖本地工厂类。com.caucho.hessian.client.HessianProxyFactory实现了ObjectFactory接口。其getObjectInstance方法会创建一个HessianProxy动态代理,并设置一个URL。当代理执行任何方法时,会触发invoke,向预设URL请求Hessian序列化数据并触发反序列化。
  • 利用条件:
    1. 目标可出网。
    2. JNDI lookup到对象后,需调用该对象的任意方法以触发invoke
  • EXP: AliyunCTF/MHGA
  • 工具支持: java-chains支持生成此类通过JNDI利用的payload。

3.2 JDK-Only链

利用链完全由JDK内置类构成。

  • 关键点:
    1. 最终的执行点(Sink)。
    2. 如何触发Sink(通常通过各种toString方法)。
  • 常见Sink:
    • SwingLazyValue#createValue (sun.swing.SwingLazyValue): 可调用任意public static方法或public构造方法。注意: JDK 11+ 中已移除。默认使用BootStrap ClassLoader,只能加载核心JAR包。
    • ProxyLazyValue#createValue (javax.swing.UIDefaults.ProxyLazyValue): 功能同上,在JDK 11+ 中仍存在,且使用AppClassLoader,适用性更广。
  • 具体Sink利用:
    • ServerManagerImpl#getActiveServers 通过Runtime.getRuntime().exec()执行命令。需对特殊符号命令进行编码。可配合getter触发。
    • ClassPathXmlApplicationContext#<init> 通过构造方法加载恶意XML(需spring-context依赖)。
    • JavaWrapper#_main (JDK < 8u251): 通过加载BCEL字节码利用。会调用恶意类中的_main方法。
    • JavaUtils#writeBytesToFilename + System#load 先写入DLL/SO文件,再加载执行。结合HashMap可实现一次请求完成。
    • DumpBytecode#dumpBytecode (Nashorn JAR): 仅适用于JDK 8,且需通过ProxyLazyValue触发。
    • MethodUtil#invoke (sun.reflect.misc.MethodUtil): 可将调用范围从public static方法扩展至所有public方法。

3.3 触发链:UIDefaults#get -> createValue

  • 核心链路: xxx#toString -> UIDefaults#get -> SwingLazyValue/ProxyLazyValue#createValue
  • 触发get的方法:
    • toString
      • sun.security.pkcs.PKCS9Attributes (attributes为可控的HashTable)
      • javax.activation.MimeTypeParameterList
    • HashTable#equals HashMap#putVal -> HashTable#equals -> UIDefaults#get

3.4 触发toString的方法

  • 通过expect触发: 当对象在字符串拼接中被隐式调用toString。在Hessian反序列化读取type失败进入default分支报错时,会反序列化对象并与字符串拼接。需手动写入特定字节构造该场景。
  • XString家族: 利用HashMap#putVal -> AbstractMap#equals -> XString#equals -> xxx#toString。常用XStringForFSB
  • AudioFileFormat.Type 通过Type#equals触发。
  • ConcurrentHashMap 可作为HashMap被禁用时的替代方案。

3.5 出网JNDI打法

  • Spring PartiallyComparableAdvisorHolderspring-aopspring-context依赖。最终触发SimpleJndiBeanFactorylookup方法。
  • Spring AbstractBeanFactoryPointcutAdvisorspring-aopspring-context依赖。同样以SimpleJndiBeanFactory#lookup为Sink。
  • Rome链: 通过ToStringBean触发getter方法,进而利用JdbcRowSetImpl(JNDI)或SignedObject(二次反序列化)。
  • Resin链: 限制较大,仅能通过RMI加载远程工厂类,适用于JDK < 8u121。其中利用XString#hashCode实现哈希碰撞的技巧值得学习。
  • XBean链: 与Resin链类似,限制较大。

3.6 Groovy相关链

  • GString链 (Hessian利用):
    • 核心: 通过groovy.lang.GStringwriteTo方法调用Closurecall方法。
    • 条件: getStrings有返回值;values中包含Closure及其子类;maximumNumberOfParameters为0。
    • 利用: 使用MethodClosure,其doCall方法可执行任意命令(如"whoami".execute())或无参方法。也可用于触发二次反序列化或JNDI。java-chains支持生成。
  • 原生反序列化利用链: 触发点可以是GString#hashCodeGString#toStringClosure#call
  • ContinuationDirContext链 (GroovyRef链): 适用于 JDK < 8u121。利用org.codehaus.groovy.runtime.ConvertedClosure作为动态代理InvocationHandler,配合TreeMap#put触发compareTo,进而调用InvocationHandlerinvoke方法。

3.7 UTF8-Overlong Encoding绕过

  • 用途: 绕过基于字符串匹配(类名匹配)的黑名单过滤。
  • 原理: 将Unicode字符(如ASCII字符)通过补零方式,编码成用两个(或更多)字节表示的非法UTF-8字符。这些字符Java可正常识别,但字符串匹配无法识别,从而隐藏类名。
  • 实现: 替换自定义的OutputStreamjava-chains提供了相关生成选项。

第四章:其他Hessian相关内容

  • Kryo反序列化: 底层基于Hessian,曾在CTF中出现。
  • Dubbo HessianServlet中的反序列化: 待补充。

参考资源

  • 本文主要代码和POC示例:https://github.com/1diot9/MyJavaSecStudy/tree/main/hessian
  • UTF8 Overlong Encoding绕过原理:https://www.leavesongs.com/PENETRATION/utf-8-overlong-encoding.html
相似文章
相似文章
 全屏