Hessian 二次反序列化新链从零到一挖掘
字数 2658
更新时间 2026-05-09 08:35:55

Hessian 二次反序列化新链从零到一挖掘教学文档

前言

本文旨在完整呈现从零开始挖掘一条Hessian二次反序列化新利用链的全过程。Hessian作为一种轻量级二进制RPC协议,因其高效和跨语言特性在分布式系统中广泛应用,但其反序列化机制存在安全隐患。本教学将系统性地讲解如何利用CodeQL等工具,从源码分析到利用链构建,最终实现漏洞复现。

第一章:前置知识

1.1 Hessian与原生Java反序列化的核心差异

理解Hessian反序列化漏洞挖掘,必须首先掌握其与标准Java反序列化的根本区别:

序列化/反序列化机制差异:

  • Java原生序列化:依赖于ObjectOutputStream/ObjectInputStreamSerializable接口。反序列化过程会自动调用对象的readObject方法,这是大多数Java反序列化利用链的入口。
  • Hessian序列化:使用自定义的紧凑二进制格式。在反序列化过程中,不会自动调用任何类的readObjectreadResolve等方法。其核心机制是通过Hessian2InputreadObject方法,根据二进制流中的类型定义,实例化对象并直接调用其setter方法(遵循JavaBean规范)来填充属性

利用链构建影响:
此差异导致传统的、依赖readObject入口的Java反序列化链(如CommonsCollectionsJdk7u21)无法直接在Hessian环境下生效。Hessian利用链必须寻找新的“源”(Source),即那些在对象属性被设置时(通过setter方法)就能触发危险操作的类。

关键结论:
Hessian反序列化漏洞的利用依赖于找到在构造函数、hashCodeequalscompareTotoString方法,或者更重要的,在setter方法自身中包含危险代码的类。

1.2 二次反序列化(Bypass)概念

当目标环境中不存在可直接导致命令执行或高危操作的“一级”利用链时,“二次反序列化”成为一种常见的绕过思路。其核心思想是:

  1. 第一次反序列化:利用Hessian机制,反序列化一个“跳板”对象。这个跳板对象包含一个属性,其setter方法能够触发另一次反序列化过程。
  2. 第二次反序列化:这次触发的是一个标准的Java原生反序列化流程。由于这次反序列化会调用readObject方法,因此所有庞大的、成熟的传统Java反序列化Gadget链(如CommonsCollections, Jdk7u21等)都可以被复用。

核心挑战:
寻找一个类,它有一个setter方法,该方法内部存在接收Object参数、byte[]参数或String(可被解码为字节数组)参数,并能够将这些参数传递给ObjectInputStream.readObject()或类似反序列化方法。

第二章:新二次反序列化利用链构建与CodeQL数据库查询全思路

本章详细阐述如何从海量源代码中,系统化地挖掘符合条件的“跳板”类。

2.1 目标代码库准备

  1. 确定目标:选择一个广泛使用、可能包含Hessian协议处理逻辑的Java项目,例如dubbo-hessian-lite(Dubbo框架中的Hessian实现)、resin应用服务器的Hessian相关模块,或者任何包含Hessian依赖的大型开源项目。
  2. 建立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方法的参数是Objectbyte[]StringInputStream等可能包含序列化数据的类型。
  • 可序列化约束:要求包含这个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查询后,会得到一个候选类列表。需要对每个候选类进行人工审计:

  1. 确认数据流:查看CodeQL提示的数据流路径是否真实、完整,没有在关键节点被截断。
  2. 分析调用上下文:确认触发反序列化的代码路径(通常在setter方法内部或setter调用的其他方法中)是可达的,没有前置条件(如if判断)在Hessian反序列化设置属性时无法满足。
  3. 评估可利用性:确认第二次反序列化(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(用于生成传统
相似文章
相似文章
 全屏