Hessian 二次反序列化新链从零到一挖掘教学文档
前言
本文旨在完整呈现从零开始挖掘一条Hessian二次反序列化新利用链的全过程。Hessian作为一种轻量级二进制RPC协议,因其高效和跨语言特性在分布式系统中广泛应用,但其反序列化机制存在安全隐患。本教学将系统性地讲解如何利用CodeQL等工具,从源码分析到利用链构建,最终实现漏洞复现。
第一章:前置知识
1.1 Hessian与原生Java反序列化的核心差异
理解Hessian反序列化漏洞挖掘,必须首先掌握其与标准Java反序列化的根本区别:
序列化/反序列化机制差异:
- Java原生序列化:依赖于
ObjectOutputStream/ObjectInputStream和Serializable接口。反序列化过程会自动调用对象的readObject方法,这是大多数Java反序列化利用链的入口。 - Hessian序列化:使用自定义的紧凑二进制格式。在反序列化过程中,不会自动调用任何类的
readObject、readResolve等方法。其核心机制是通过Hessian2Input的readObject方法,根据二进制流中的类型定义,实例化对象并直接调用其setter方法(遵循JavaBean规范)来填充属性。
利用链构建影响:
此差异导致传统的、依赖readObject入口的Java反序列化链(如CommonsCollections、Jdk7u21)无法直接在Hessian环境下生效。Hessian利用链必须寻找新的“源”(Source),即那些在对象属性被设置时(通过setter方法)就能触发危险操作的类。
关键结论:
Hessian反序列化漏洞的利用依赖于找到在构造函数、hashCode、equals、compareTo、toString方法,或者更重要的,在setter方法自身中包含危险代码的类。
1.2 二次反序列化(Bypass)概念
当目标环境中不存在可直接导致命令执行或高危操作的“一级”利用链时,“二次反序列化”成为一种常见的绕过思路。其核心思想是:
- 第一次反序列化:利用Hessian机制,反序列化一个“跳板”对象。这个跳板对象包含一个属性,其setter方法能够触发另一次反序列化过程。
- 第二次反序列化:这次触发的是一个标准的Java原生反序列化流程。由于这次反序列化会调用
readObject方法,因此所有庞大的、成熟的传统Java反序列化Gadget链(如CommonsCollections,Jdk7u21等)都可以被复用。
核心挑战:
寻找一个类,它有一个setter方法,该方法内部存在接收Object参数、byte[]参数或String(可被解码为字节数组)参数,并能够将这些参数传递给ObjectInputStream.readObject()或类似反序列化方法。
第二章:新二次反序列化利用链构建与CodeQL数据库查询全思路
本章详细阐述如何从海量源代码中,系统化地挖掘符合条件的“跳板”类。
2.1 目标代码库准备
- 确定目标:选择一个广泛使用、可能包含Hessian协议处理逻辑的Java项目,例如
dubbo-hessian-lite(Dubbo框架中的Hessian实现)、resin应用服务器的Hessian相关模块,或者任何包含Hessian依赖的大型开源项目。 - 建立CodeQL数据库:使用CodeQL CLI工具对目标项目的源代码创建数据库。命令示例如下:
codeql database create <database-path> --language=java --command="mvn clean compile" --source-root=<project-root>
2.2 CodeQL查询设计与分析
查询的核心是定位满足“二次反序列化跳板”条件的类。查询思路分层递进:
第一层:定位潜在的触发方法
首先寻找所有可能触发反序列化的方法调用,如ObjectInputStream.readObject()、JSON.parseObject()(某些库的JSON解析可能导致反序列化)、或者任何自定义的deserialize方法。
from MethodAccess ma, Method m
where
ma.getMethod() = m and
(
m.hasName("readObject") or
m.hasName("parseObject") or
m.getDeclaringType().hasQualifiedName("某个包", "Unsafe反序列化类")
)
select ma, "Potential deserialization sink found."
第二层:溯源至Setter方法
从上述的“危险方法调用点”(Sink)开始,向上追溯数据流,检查是否有数据源来自某个类的setter方法的参数。
import java
import semmle.code.java.dataflow.DataFlow
predicate isSetter(Method m) {
m.getName().matches("set%") and // 方法名以set开头
m.getNumberOfParameters() = 1 // 只有一个参数
}
from Method setter, Parameter param, Expr sink
where
isSetter(setter) and
param = setter.getParameter(0) and // setter方法的第一个(也是唯一)参数
DataFlow::localFlow(DataFlow::exprNode(param), DataFlow::exprNode(sink)) and // 参数数据流到了sink
exists(MethodAccess ma | ma = sink.getEnclosingCallable() | ma.getMethod().hasName("readObject")) // sink是readObject调用
select setter, "Found setter that may flow its argument to readObject: " + setter.getDeclaringType().getName() + "." + setter.getName()
第三层:精炼查询,增加约束
- 参数类型约束:要求setter方法的参数是
Object、byte[]、String、InputStream等可能包含序列化数据的类型。 - 可序列化约束:要求包含这个setter方法的类本身实现了
Serializable接口,因为Hessian虽然不要求Serializable,但作为JavaBean,它通常会被序列化。 - 库/框架约束:可以限定搜索范围在特定的、已知会处理序列化数据的包中,例如
com.caucho.hessian.io.*,com.alibaba.com.caucho.hessian.io.*等。
一个更完整的查询示例框架:
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.TaintTracking
class DeserializationSink extends MethodAccess {
DeserializationSink() {
exists(Method m |
this.getMethod() = m and
m.hasName("readObject") and
m.getDeclaringType().hasQualifiedName("java.io", "ObjectInputStream")
)
}
}
class SetterToSinkTaintConfig extends TaintTracking::Configuration {
SetterToSinkTaintConfig() { this = "SetterToSinkTaintConfig" }
override predicate isSource(DataFlow::Node source) {
exists(Method m, Parameter p |
m.getName().matches("set%") and
m.getNumberOfParameters() = 1 and
p = m.getParameter(0) and
p.getType() instanceof TypeObject or // Object类型
p.getType() instanceof TypeByteArray or // byte[]类型
p.getType().hasQualifiedName("java.lang", "String") and // String类型
source.asParameter() = p
)
}
override predicate isSink(DataFlow::Node sink) {
exists(DeserializationSink ds |
sink.asExpr() = ds.getAnArgument() // 假设参数是危险方法的输入
or
sink.asExpr() = ds.getQualifier() // 或者是调用对象本身
)
}
}
from SetterToSinkTaintConfig config, DataFlow::Node source, DataFlow::Node sink
where
config.hasFlow(source, sink)
select sink, source,
"Data flow from setter parameter $@ to deserialization sink $@.",
source, source.toString(),
sink, sink.toString()
第四层:人工审计确认
运行CodeQL查询后,会得到一个候选类列表。需要对每个候选类进行人工审计:
- 确认数据流:查看CodeQL提示的数据流路径是否真实、完整,没有在关键节点被截断。
- 分析调用上下文:确认触发反序列化的代码路径(通常在setter方法内部或setter调用的其他方法中)是可达的,没有前置条件(如if判断)在Hessian反序列化设置属性时无法满足。
- 评估可利用性:确认第二次反序列化(
readObject)的输入数据(即setter方法的参数)是否完全可控。在Hessian场景下,这意味着攻击者可以通过精心构造的Hessian二进制流,为这个属性设置一个恶意的、经过一次序列化的byte[]或String(Base64编码的序列化数据)。
第三章:新二次反序列化链完整复现
本节以一个理论上的候选类com.example.VulnerableBean为例,演示完整复现过程。注意:以下类名、方法名为示意,实际操作中需替换为通过CodeQL挖掘出的真实类。
3.1 环境与工具准备
- Java环境:JDK 8。
- Hessian库:目标系统使用的Hessian实现,例如
hessian-4.0.66.jar。 - 序列化工具:用于生成最终Payload的Java程序,以及
ysoserial(用于生成传统