Hessian反序列化漏洞深度解析:流程、修复与利用链大全
字数 5933
更新时间 2026-03-07 12:11:20
Hessian 序列化与反序列化安全漏洞解析与利用链总览
本文档基于 阿里云先知社区文章 内容整理
前言
本文主要目标是:
- 整理Hessian 1/2的序列化与反序列化流程。
- 探讨Hessian反序列化漏洞的临时修复方案。
- 收集Hessian反序列化中的各种利用链(Gadgets)与绕过技巧。
- 补充其他Hessian相关的知识点。
第一章:序列化与反序列化流程
1.1 序列化过程
代码位置: GitHub示例代码库
环境说明: 序列化工厂设置允许序列化未实现Serializable接口的类。
1.1.1 HessianOutput
- 入口:
com.caucho.hessian.io.HessianOutput#writeObject - 流程:
- 获取序列化器: 判断序列化对象非空后,根据对象所属类,调用
_serializerFactory.getSerializer获取序列化器。 - 序列化器缓存机制:
SerializerFactory.getSerializer优先从缓存Map中查找。若无,则调用loadSerializer。 - 加载序列化器 (
loadSerializer):- 第295行:
_contextFactory.getSerializer(cl.getName())根据类名直接获取39个基本类型及其包装类的序列化器。 - 第309行:尝试获取自定义序列化器 (
XXXXHessianSerializer)。 - 经过一系列
if判断特定类型后,未匹配的自定义类最终走到getDefaultSerializer。
- 第295行:
- 默认序列化器 (
getDefaultSerializer):- 未设置默认序列化器时,判断类是否实现
Serializable接口,或工厂设置了_isAllowNonSerializable(解除序列化限制的关键)。 - 默认情况下,会在392行创建
UnsafeSerializer。
- 未设置默认序列化器时,判断类是否实现
UnsafeSerializer初始化 (create与introspect):- 通过反射“内省”获取类中所有公开属性和方法。
- 第124行:属性若被
transient或static修饰,则不参与序列化。这是利用链中不能依赖transient/static变量的原因(如TemplatesImpl的_tfactory)。
- 写入对象: 序列化器就绪后,调用
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() - 流程:
- 判断类型: 通过
switch进入相应分支。自定义对象和Map对象写入的第一个字节都是M,故进入M对应的case。 - 获取反序列化器: 调用
SerializerFactory.readMap->getDeserializer。其过程与getSerializer类似,最终获取UnsafeDeserializer。 UnsafeDeserializer初始化: 在构造方法中通过反射获取类属性,将属性名和对应的反序列化器放入fieldMap。- 实例化与属性还原: 调用
instantiate,通过Unsafe直接实例化对象,然后调用readMap还原各个属性。反序列化器从fieldMap取出,通过Unsafe方法根据内存偏移量直接设置值。
- 判断类型: 通过
HashMap的反序列化: 稍有不同。序列化时写入的type为null,最终调用MapDeserializer。其中map.put是关键利用入口,可触发key.hashCode、key.equals(HashMap)或compareTo(TreeMap)。
1.2.2 HessianInput2
- 流程: 同样通过
switch判断类型。 - 关键差异: 先读取类名、属性数量并还原属性定义(不设值),将定义包装为
ObjectDefinition存入_classDefs。随后在readObject中读取并设置属性值。对于自定义对象,不再默认解析为Map类型。
1.3 流程小结与利用条件
通过上述分析,得出Hessian反序列化利用的核心条件:
- 入口点(Source): 必须是
hashCode、equals或compareTo方法。 - 属性限制: 利用链中不能有
transient或static修饰的变量。
第二章:临时修复方案
主要通过重写SerializerFactory,添加恶意类的黑名单过滤,阻止不安全的类被反序列化。
第三章:利用链总结
本章对各类利用链原理进行简要分析,并附上参考代码。
3.1 HessianProxyFactory (JNDI利用)
- 来源: AliyunCTF 2026
- 原理: 高版本JDK中,JNDI利用常依赖本地工厂类。
com.caucho.hessian.client.HessianProxyFactory实现了ObjectFactory接口。其getObjectInstance方法会创建一个HessianProxy动态代理,并设置一个URL。当代理执行任何方法时,会触发invoke,向预设URL请求Hessian序列化数据并触发反序列化。 - 利用条件:
- 目标可出网。
- JNDI
lookup到对象后,需调用该对象的任意方法以触发invoke。
- EXP: AliyunCTF/MHGA
- 工具支持:
java-chains支持生成此类通过JNDI利用的payload。
3.2 JDK-Only链
利用链完全由JDK内置类构成。
- 关键点:
- 最终的执行点(Sink)。
- 如何触发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
PartiallyComparableAdvisorHolder: 需spring-aop、spring-context依赖。最终触发SimpleJndiBeanFactory的lookup方法。 - Spring
AbstractBeanFactoryPointcutAdvisor: 需spring-aop、spring-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.GString的writeTo方法调用Closure的call方法。 - 条件:
getStrings有返回值;values中包含Closure及其子类;maximumNumberOfParameters为0。 - 利用: 使用
MethodClosure,其doCall方法可执行任意命令(如"whoami".execute())或无参方法。也可用于触发二次反序列化或JNDI。java-chains支持生成。
- 核心: 通过
- 原生反序列化利用链: 触发点可以是
GString#hashCode、GString#toString或Closure#call。 ContinuationDirContext链 (GroovyRef链): 适用于 JDK < 8u121。利用org.codehaus.groovy.runtime.ConvertedClosure作为动态代理InvocationHandler,配合TreeMap#put触发compareTo,进而调用InvocationHandler的invoke方法。
3.7 UTF8-Overlong Encoding绕过
- 用途: 绕过基于字符串匹配(类名匹配)的黑名单过滤。
- 原理: 将Unicode字符(如ASCII字符)通过补零方式,编码成用两个(或更多)字节表示的非法UTF-8字符。这些字符Java可正常识别,但字符串匹配无法识别,从而隐藏类名。
- 实现: 替换自定义的
OutputStream。java-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
相似文章
相似文章