Hessian反序列化
发表于:2025-09-08 | 分类: Java

Hessian反序列化

前言

最近来学习一下Hessian反序列化,感觉这个组件也是非常常见的,看了看感觉有点类似于RMI的远程方法调用,但是触发点并不是readObject了,变成了hashCode/equals/compareTo其中之一

Hessian介绍

Hessian 是一种基于二进制的轻量级网络传输协议,用于在不同的应用程序之间进行远程过程调用(RPC)。它是由 Caucho Technology 开发的,并在 Java 社区中得到广泛应用。
Hessian 的设计目标是提供一种高效、简单和可移植的远程调用协议。相比于其他文本协议如 XML-RPC 或 SOAP,Hessian 使用二进制格式进行数据序列化和网络传输,可以实现更高的性能和较小的网络传输开销。这也使得 Hessian 在低带宽或高延迟的网络环境下表现出色。

基本使用

环境搭建

首先创建需要一个Java项目,为其配置一个Tomcat(如果是Spring项目,创建Spring即可)

版本如下:

  • Tomcat 8.5.76
  • JDK8u65

我们需要配置pom.xml,导入Hession依赖

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.59</version>
</dependency>

使用示例

Hessian也是基于HTTP协议的,也通过Web应用提供服务,一般的使用场景有以下几种

基于Servlet项目

继承类

在Servlet项目中,可以将提供服务的类注册成Servlet的方式作为服务端进行交互

在Server端和Client端,都需要去构造同一个接口

1
2
3
public interface Greeting {
String sayHello(HashMap map);
}

在服务端中,需要有一个该方法的具体实现,通过继承HessianServlet类,实现Greeting接口并重写具体方法,构造成一个提供服务的Servlet

1
2
3
4
5
6
public class HelloServlet extends HessianServlet implements Greeting{
@Override
public String sayHello(HashMap map) {
return "Hello" + map.toString();
}
}

然后在web.xml中配置Servlet的映射

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>hessian</servlet-name>
<servlet-class>com.hessian.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hessian</servlet-name>
<url-pattern>/hessian</url-pattern>
</servlet-mapping>

在Client端通过HessianProxyFactory工厂类创建对应接口的代理对象,然后进行调用

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) throws MalformedURLException {
String url = "http://127.0.0.1:8080/hessian";
HessianProxyFactory factory = new HessianProxyFactory();
Greeting greeting = (Greeting) factory.create(Greeting.class, url);

HashMap hashMap = new HashMap<>();
hashMap.put("name", "CurlySean");

System.out.println("Call:" + greeting.sayHello(hashMap));
}
}

启动Tomcat环境后,运行Client端代码,可以看到成功执行了服务端的逻辑,并返回实现方法的返回值

上面说Hessian是基于HTTP协议的,这里通过WireShark追踪HTTP流看一下

配置文件

服务类也可以不继承HessianServlet类,我们可以通过直接配置web.xml来设置

首先将服务类改成以下这样

1
2
3
4
5
6
public class HelloServlet implements Greeting{
@Override
public String sayHello(HashMap map) {
return "Hello" + map.toString();
}
}

然后我们修改web.xml中的映射配置,即可达到一样的效果

1
2
3
4
5
6
7
8
9
10
11
12
<servlet>
<servlet-name>hessian</servlet-name>
<servlet-class>com.hessian.HelloServlet</servlet-class>
<init-param>
<param-name>home-class</param-name>
<param-value>com.hessian.HelloServlet</param-value>
</init-param>
<init-param>
<param-name>home-api</param-name>
<param-value>com.hessian.Greeting</param-value>
</init-param>
</servlet>

基于Spring项目

在Spring-Web包中提供了HessianServiceExporter类来暴露远程调用的接口和实现类,使用该类export的HessianService可以被任何HessianClient访问

从SpringWeb5.3之后,该类被标记为了@Deprecated,Spring逐渐淘汰了对基于序列化的远程调用的相关支持

注解

同样我们需要构造接口和具体实现类,是和上面一样的,任何我们使用注解来实现服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootApplication
public class SprHessianApplication {

@Autowired
private Greeting greeting;

public static void main(String[] args) {
SpringApplication.run(SprHessianApplication.class, args);
}

@Bean(name = "/hessian")
public HessianServiceExporter accountService() {
HessianServiceExporter exporter = new HessianServiceExporter();
exporter.setService(greeting);
exporter.setServiceInterface(Greeting.class);
return exporter;
}
}

除此之外,还需要给HelloServlet类添加一个@Component注解,用于自动装配

1
2
3
4
5
6
7
@Component
public class HelloServlet extends HessianServlet implements Greeting{
@Override
public String sayHello(HashMap map) {
return "Hello" + map.toString();
}
}

最后我们的Client端不变即可

配置文件

除了注解以外,我们可以使用配置文件方式进行实现(就是有点麻烦)

首先还是创建服务接口

1
2
3
public interface MyService {
String sayHello();
}

然后实现接口服务,并重写方法实现具体业务逻辑

1
2
3
4
5
6
public class MyServiceImpl implements MyService {
@Override
public String sayHello() {
return "Hello, Hessian!";
}
}

然后再Spring的配置文件中来配置Hessian的Server端

1
2
3
4
5
6
<bean name="/myService" class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service" ref="myService"/>
<property name="serviceInterface" value="com.example.MyService"/>
</bean>

<bean id="myService" class="com.example.MyServiceImpl"/>

在Spring中也可以配置Clinet端,向Spring配置文件中添加如下配置

1
2
3
4
<bean id="myServiceProxy" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
<property name="serviceUrl" value="http://localhost:8080/myService"/>
<property name="serviceInterface" value="com.example.MyService"/>
</bean>

远程调用服务

1
2
3
4
5
6
7
@Autowired
private MyService myServiceProxy;

public void doSomething() {
String result = myServiceProxy.sayHello();
System.out.println(result);
}

自封装使用

除了配合Web项目使用,也可以通过对Hessian内相关方法封装,自行实现传输存储等逻辑,使用 Hessian 进行序列化和反序列化数据
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
public class HessianUtil {
public static byte[] hessianSerialize(Object obj){
Hessian2Output output = null;
byte[] bytes = null;

try{
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
output = new Hessian2Output(outputStream);
output.writeObject(obj);
output.flush();
output.close();
bytes = outputStream.toByteArray();
}catch (Exception e){
e.printStackTrace();
}
return bytes;
}

public static Object hessianDeserialize(byte[] bytes){
Object obj = null;
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
obj = hessian2Input.readObject();
}catch (Exception e){
e.printStackTrace();
}
return obj;
}
}

JNDI源配置

Hessian 还可以通过将 HessianProxyFactory 配置为 JNDI Resource 的方式来调用(之前没怎么见过这种方式)

例如我们可以在resin.xml中添加以下配置

1
2
3
4
5
6
<reference>
<jndi-name>hessian/jndi</jndi-name>
<factory>com.caucho.hessian.client.HessianProxyFactory</factory>
<init-parm url="http://localhost:8080/hessian"/>
<init-parm tyep="com.hessian.Greeting"/>
</reference>

然后我们可以使用JNDI查询的方法进行调用,代码如下

1
2
3
4
5
Context ic = new InitialContext();
Greeting hello = (Greeting) ic.lookup("java:comp/env/hessian/jndi");
HashMap<String, String> o = new HashMap<String, String>();
o.put("a", "c");
System.out.println("Hello: " + hello.sayHello(o));

源码分析

服务端接口

基于Servlet

在上面基本使用时,我们的Servlet需要继承HessianServlet类,而HessianServletHttpServlet的子类,因此我们主要关注的有两个方法:

  • init:初始化功能
  • service:相关处理的起始位置
init方法

init方法总体来说就是用来初始化HessianServlet的成员变量,例如:_homeAPI(调用类的接口Class)、 _homeImpl(具体实现类的对象)、_serializerFactory(序列化工厂类)、_homeSkeleton(封装方法)等等

我的IDEA中截图截不全,这里借用一下su18大佬的图

service方法
我们知道对于Servlet,其中最重要的业务逻辑代码就在`service`方法中

进入invoke方法后,会根据objectId是否为空,来判断调用哪个Skeleton

在objectId为空时,我们会走到HessianSkeleton#invoke方法中,首先会读取头部信息,根据头部信息判断使用哪种协议类型进行数据交换

最后创建好对于的输入流和输出流后,进行下一步的调用

跟进到invoke方法里面,会查找对应方法、根据参数类型反序列化参数值

在最后的method.invoke方法中,就会反射调用远程获取的方法

基于Spring

在Spring项目中,关键类为HessianExporter,关键方法是doInvoke,但是里面的处理逻辑是相类似的

序列化和反序列化流程

在反序列化漏洞分析时,关键类包括:输入流/输出流、序列化器/反序列化器、相关工厂类

这里我们写一段序列化和反序列化的测试代码,因为这条链子的入口用到的是HashMap类,因此这里对HashMap作为我们的测试对象,我们用Hessian2Output/Hessian2Input为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SerializeTest {
public static void main(String[] args) throws IOException {
// 先序列化
HashMap hashMap = new HashMap<>();
hashMap.put("name", "CurlySean");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeObject(hashMap);
hessian2Output.close();
System.out.print(byteArrayOutputStream.toString() + "\n");

// 反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
Object student1 = hessian2Input.readObject();
System.out.println(student1.toString());
}
}

程序运行结果如下

序列化流程

我们在`hessian2Output.writeObject`方法处下一个断点,来调试序列化流程

跟进writeObject方法后,该方法做了两个操作,一个操作是获取对应类型的序列化器,另一个操作就是用该序列化器对传入的类进行序列化操作

跟进getObjectSerializer方法,再次跟进getSerializer

这里首先从缓存中找是否存在该类对应的序列化器,当没有的时候就调用loadSerializer方法获取对应序列化器,并放进缓存中

进入loadSerializer方法后,从_contextFactory中找到对应的序列化器(可以看到里面有39个序列化器,对应着不同类)

在对于自定义类型,将会使用 JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer 进行相关的序列化动作,默认情况下是 UnsafeSerializer

获取到MapSerializer序列化器后,使用其对Map进行序列化操作

首先调用writeMapBegin方法,写入Map类序列化数据头部,然后遍历键值对,继续调用writeObject方法将键值写入输出流中,这里也会根据键值的类型,找到对应的序列化器进行序列化,最后调用writeMapEnd方法写入Map类序列化数据尾部

反序列化流程

同样我们在`readObject`处下一个断点

首先读取头部信息,根据头部信息判断所反序列化的类

进入readMap方法,在_hashMapDerializer为空的情况下,会传入HashMap.class来new一个MapDeserializer类作为HashMap的反序列化器

readMap方法中,会根据_type创建对应的Map类,这里我们会构造HashMap类,后面调用in.readObejct方法反序列化出键值对,利用put方法放入HashMap中,前面我们写入了序列化数据的对应尾部,这里调用readEnd方法读取尾部信息

这样我们的反序列化流程就走完了,这里以Map类为例,其他类有其对应的反序列化流程,可以自行调试一下

有人要问了,在学习其他组件反序列化链时,我们不是一般都会想办法利用某个类的getter方法来触发我们的反序列化链吗,为什么这次没有看到getter的身影呢?

这里是因为Hessian反序列化中,并没有构造方法、setter和getter方法的调用,对于值的获取与设置,是通过反射进行的,可以自行调试一下

漏洞点

在反序列化的过程中,构造方法、getter/setter方法都不会被触发,那么我们该怎么触发反序列化链呢?

在之前的反序列化链中,有些链子的入口点在HashMap#readObejct中,我们当时以hash方法为入口点

那么学过的师傅应该还有印象,正常我们在序列化的过程中,调用到put方法时也是会触发我们的链子的,因为put方法中也调用了hash方法

在对于Map类的序列化器中,还可以处理TreeMap类,在其put方法中会调用compareTo方法,对key进行比较操作

那么相比于原生的反序列化利用链,有以下几个限制:

  • 利用链的其实方法为 hashCode/equals/compareTo
  • 利用链中的成员变量不能为transient修饰
  • 所有的调用中不依赖类中的readObject逻辑,也不依赖 getter/setter 的逻辑

利用链

ROME链

在Rome链中的核心为ToStringBean类,该类的toString方法会调用其封装类的所有无参getter方法

1
2
3
4
5
<dependency>  
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>

JdbcRowSetImpl 利用链

因此我们可以调用JdbcRowSetImpl#getDatabaseMetaData方法

getDatabaseMetaData方法中调用了connect方法,在此处调用了lookup方法,且getDataSourceName是可控的,在这里我们可以触发JNDI注入

二次反序列化

利用JNDI注入是需要出网的,但是其一目标机器可能不出网,其二是有设备的检测,JNDI很容易触发告警,因此我们可以使用Rome链进行二次反序列化来去除出网的限制

二次反序列化的调用栈如下

1
2
3
4
5
6
7
hessianInput.readObject()
MapDeserializer.readMap()
map.put()
ObjectBean.hashCode()
ToStringBean.toString()
SignedObject.getObject()
···

这里我们看到关键的SignedOb,ject#getObject方法中,会调用readObejct原生反序列化,反序列化this.content 数据流,触发二次反序列化

总结

除了Rome链以外,还有一些其他利用链,例如:Resin和Xbean、Spring AOP、Groovy等,但是目前来说只学习了Rome链,其他的慢慢补吧

上一篇:
SpringAop链
下一篇:
高版本JDKSpring原生反序列化链