JavaSec-Java反序列化基础篇01-反序列化概念与利用

学习大纲

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
1. 概念
1.1. 序列化与反序列化
1.2. 为何要进行序列化和反序列
1.3. 反序列化为何产生的安全问题
1.4. 序列化的好处
1.5. 序列化和反序列的应用场景
2. 序列化与反序列化代码实现
2.1. 代码展示
2.1.1. 类文件:Person.java
2.1.2. 序列化文件:SerializtionTest.java
2.1.3. 反序列化文件:UnserializeTest.java
3. ObjectOutputStream
3.1. ObjectOutputStream继承的父类和实现的接口
3.1.1. writeObject
3.1.2. writeObject0()
4. ObjectInputStream
4.1. ObjectInputStream继承的父类和实现的接口
4.1.1. readObject
5. 序列化与反序列化代码解析
5.1. 基本实现
5.1.1. SerializationTest.java
5.1.2. UnserializationTest.java
5.2. Serializable接口
5.2.1. 1.序列化类在没有实现Serializable接口时在进行序列化时会报错
5.2.2. 2.静态成员变量是不能被序列化的
5.2.3. 3.transient标识的对象成员变量不参与序列化
6. 反序列化为什么会产生安全问题
6.1. 入口类的readObject直接调用恶意代码
6.1.1. 通过debug探究readObject是如何执行恶意代码的
6.1.1.1. 1.跟进到readObject()
6.1.1.2. 2.readObject()调用readObject0()
6.1.1.3. 3.继续调用readOrdinaryObject()
6.1.1.4. 4.调用readSerialDate()
6.1.1.5. 5.调用invokeReadObject()
6.1.2. debug过程中的源码分析
6.1.2.1. 1.readObject()
6.1.2.2. 2.readObject0()
6.1.2.3. 3.readOrdinaryObject()
6.1.2.4. 4.readSerialDate()
6.1.2.5. 5.invokeReadObject()
6.1.3. tips
7. 攻击路线
7.1. 要满足的一些条件
7.2. 如何找到入口类
7.2.1. HashMap
7.2.1.1. 1.继承Serializable接口
7.2.1.2. 2.包含重写的readObject()方法
7.2.1.3. 3.调用常见的函数

概念

在开始之前想聊一些关于概念的东西,比如何为反序列化,为什么要进行反序了化,序列化的好处和什么情况下需要使用到序列化和反序列化和其为什么会产生安全问题。

序列化与反序列化

Java中的序列化与反序列化,就是将一个Java对象当前的状态(即为该对象的属性)以字符串(即为字节序列)的形式描述出来;然后该字符串可能会被传递or存储到某个地方,在我们需要的时候在将这串字节序列还原为原来的Java对象。在两者的转换之间需要一条规则;规则中描述了序列化和反序列化时如何将对象转换成字节序列,又如何将字节序列转换成对象。因为这两者之间是可逆的。

所以综上所述,Java序列化就是将Java对象转换为字节序列;其中ObjectOutputStreamwriteObject()方法可以实现序列化。Java反序列化就是将字节序列转换为Java对象,其中ObjectInputStreamreadObject()方法可以实现序列化。

为何要进行序列化和反序列

当两个进程进行通讯时我们知道其可以互相传递各种数据,包括文本、图片、音频、视频等;这些数据都会以二进制的形式在网络中传输。而当两个Java进程进行通讯时,同样可以实现两个对象的传输。这其中是如何实现的,这就需要Java的序列化和反序列化了;发送方需要把这个Java对象进行序列化转换为字节序列进行传输,在接收方收到这个字节序列时需要对其进行反序列化将其还原为原来的Java对象。

反序列化为何产生的安全问题

只要服务器在接受到字节序列然后对他进行反序列化时,客户端传递类的readObject()中的代码会自动执行,这就给予了攻击者可以构造恶意代码然后使其在服务器上运行从而达到攻击效果。

序列化的好处

1.能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。

2.利用序列化实现远程通信,在网络上传送对象的字节序列。

序列化和反序列的应用场景

  1. 想把内存中的对象保存到一个文件中或者是数据库当中。
  2. 用套接字在网络上传输对象。
  3. 通过 RMI 传输对象的时候。

序列化与反序列化代码实现

代码展示

类文件: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
import java.io.Serializable;

//实现 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
//重写了 Object 类中的 toString() 方法
public String toString(){
return "Person{" +
"name = '" + name + '\'' +
",age = " + age + '}';
}
}

序列化文件:SerializtionTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SerializationTest{
//定义了一个 serilalize方法,该方法接受一个 Object obj类型参数并会抛出异常
public static void serialize(Object obj) throws IOException{
//在方法内部定义了一个 ObjectOutputStream 对象 oos,用于将对象写入 ser.bin
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
//将传入的对象进行序列化操作
oos.writeObject(obj);
}

//程序入口
public static void main(String[] args) throws Exception{
//创建一个 Person 对象 person 且参数为("hey",22)
Person person = new Person("hey",22);
System.out.println(person);
//调用 serialize 将传入的 person 对象进行序列化操作;通过调用 serialize(person) 可以触发序列化
serialize(person);
}
}

反序列化文件:UnserializeTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.FileInputStream;
import java,io,IOException;
import java.io.ObjcetInputStream;

public class UnserializeTest{
//定义 unserialize 方法,接受一个 String 类型的 File 参数并会抛出异常
public static Object unserialize(String File) throws OException,ClassNotFoundException{
//创建一个 ObjectInputStream 对象 ois,用于从文件中读取对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
//使用 ois.readObject()读取对象并返回
Object obj = ois.readObject();
return obj;
}

//程序入口
public static void main(String[] args) throws Exception{
//调用 unserialize 将之前序列化的数据从 ser.bin 反序列化出来
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}

ObjectOutputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ObjectOutputStream
extends OutputStream implements ObjectOutput, ObjectStreamConstants
{

private static class Caches {
/** cache of subclass security audit results */
static final ConcurrentMap<WeakClassKey,Boolean> subclassAudits =
new ConcurrentHashMap<>();

/** queue for WeakReferences to audited subclasses */
static final ReferenceQueue<Class<?>> subclassAuditsQueue =
new ReferenceQueue<>();
}

ObjectOutputStream继承的父类和实现的接口

父类OutputStream;所有字节输出流的顶级父类,用来接收输出的字节并发送到某些接收器(sink)。

接口ObjectOutput接口扩展了DataOutput接口,DataOutput接口提供了将任何Java对象转换成二进制流的功能。而ObjectOutput在其基础上提供了writeObject方法,也就是类的写入

接口ObjectStreamConstants定义了一些在对象序列化时写入的常量。常见的一些的比如 STREAM_MAGICSTREAM_VERSION 等。

通过这个类继承的父类和实现的接口我们可以得知这个类主要实现的功能就是将Java对象的一系列特征转换成可输出流,也就是我们所说的序列化。

writeObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}

writeObjectObjectOutputStream的主要方法之一;其功能即为将Java对象写入输出流。任何对象,包括字符串和数组,都是用 writeObject 写入到流中的。

之前说过,序列化的过程,就是将一个对象当前的状态描述为字节序列的过程,也就是 Object -> OutputStream 的过程,这个过程由 writeObject 实现。writeObject 方法负责为指定的类编写其对象的状态,以便在后面可以使用与之对应 readObject 方法来恢复它。

writeObject0()

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
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
// handle previously written and non-replaceable objects
int h;
if ((obj = subs.lookup(obj)) == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}

// check for replacement object
Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
desc = ObjectStreamClass.lookup(cl, true);
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}

// if object replaced, run through original checks a second time
if (obj != orig) {
subs.assign(orig, obj);
if (obj == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}

// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}

writeObjectwriteUnshared 实际上调用 writeObject0 方法,也就是说 writeObject0是上面两个方法的基础实现。

ObjectInputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants
{
/** handle value representing null */
private static final int NULL_HANDLE = -1;

/** marker for unshared objects in internal handle table */
private static final Object unsharedMarker = new Object();

/** table mapping primitive type names to corresponding class objects */
private static final HashMap<String, Class<?>> primClasses
= new HashMap<>(8, 1.0F);
static {
primClasses.put("boolean", boolean.class);
primClasses.put("byte", byte.class);
primClasses.put("char", char.class);
primClasses.put("short", short.class);
primClasses.put("int", int.class);
primClasses.put("long", long.class);
primClasses.put("float", float.class);
primClasses.put("double", double.class);
primClasses.put("void", void.class);
}

ObjectInputStream继承的父类和实现的接口

父类InputStream,所有字节输入流的顶级父类

接口ObjectInputObjectInput 扩展了 DataInput 接口,DataInput 接口提供了从二进制流读取字节并将其重新转换为 Java 基础类型的功能,ObjectInput 额外提供了 readObject 方法用来读取类。

readObject

ObjectInputStream 读取一个对象,将会读取对象的类、类的签名、类的非 transient 和非 static 字段的值,以及其所有父类类型。

我们可以使用 writeObjectreadObject 方法为一个类重写默认的反序列化执行方,所以其中 readObject 方法会 “传递性” 的执行,也就是说,在反序列化过程中,会调用反序列化类的 readObject 方法,以完整的重新生成这个类的对象。

序列化与反序列化代码解析

基本实现

SerializationTest.java

这里我们将代码进行封装,将序列化功能封装进serialize这个方法中,在序列化当中我们通过FileOutputStream输出流对象,将序列化的对象输出到ser.bin中;接着调用ooswriteObject()方法,将对象进行序列化操作。

1
2
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);

UnserializationTest.java

进行反序列化

1
2
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();

Serializable接口

此时想聊点重要的东西;通过ObjectOutputStream将需要序列化数据写入到流中,因为Java IO是一种装饰者模式,因此可以通过ObjectOutStream包装FileOutStream将数据写入到文件中或者包装ByteArrayOutStream将数据写入到内存中。同理,可以通过ObjectInputStream将数据从磁盘FileInputStream 或者内存ByteArrayInputStream读取出来然后转化为指定的对象即可。

1.序列化类在没有实现Serializable接口时在进行序列化时会报错

1
2
3
4
5
6
7
//实现Serializable接口
public class Person implements Serializable{
}
//没有实现Serializable接口
public class Person{

}

此时的Serializable用来标识当前类可以被ObjectOutputStream进行序列化,以及被ObjectInputStream进行反序列化。

当我们将Serializable删除时便会出现报错

2.静态成员变量是不能被序列化的

序列化是针对对象属性的,而静态成员属于类的。

3.transient标识的对象成员变量不参与序列化

此时我们在类文件中的成员变量里加入transient标识

然后先进行序列化再进行反序列化可以发现再name里面已经变成null

反序列化为什么会产生安全问题

我们知道,一个类想要实现序列化和反序列化必须实现SerializableExternalizable接口;Serializable接口是一个标记接口,标记了这类可以被序列化和反序列化;而Externalizable接口在Serializable的基础上又提供了writeExternalreadExternal,用来序列化和反序列化一些外部元素。

其中如果被序列化的类重写了writeObjectreadObject方法;Java会委托这两个方法来进行序列化和反序列化。而正是因为这个特性,在反序列化的类中如果重写了readObject方法;在服务端进行反序列化时便会调用它,如果这个方法中存在一些恶意构造的代码那么便会实现攻击者的目的。

这时候我们先举一个最简单的例子来看看

入口类的readObject直接调用恶意代码

虽然这种情况在开发中基本不可能出现,但是我们作为初学者还是老老实实的将其过一遍。

此时我们重写readObject方法,在方法中插入恶意代码使用Runtime执行命令弹出计算器。

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
import java.io.IOException;
import java.io.Serializable;
import java.io.ObjectInputStream;

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

private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
//调用 ObjectInputStream 的 defaultReadObject() 方法负责读取默认的对象字段值。这样可以确保对象的默认字段值被正确反序列化。
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}

运行之后即可发现我们插入的恶意代码已经成功执行了,这个是我们攻击者最理想的漏洞。接下来我会通过idea的动调来探究恶意代码的执行过程。

通过debug探究readObject是如何执行恶意代码的

此时我们将断点下在Person person = (Person)unserialize("ser.bin");这里

1.跟进到readObject()

此时跟进到readObject()方法

2.readObject()调用readObject0()

跟进去,此时发现readObject()方法实际调用了readObject0()方法来反序列化字符串

3.继续调用readOrdinaryObject()

此时readObject0()方法以字节的方式去读取,如果读取到0x73则代表这是一个反序列化数据,则会继续调用readOrdinaryObject()方法进行处理

4.调用readSerialDate()

跟进readOrdinaryObject()方法,此时readOrdinaryObject() 方法会调用 readClassDesc() 方法读取类描述符,并根据其中的内容判断类是否实现了Externalizable接口,如果是,则调用 readExternalData 方法去执行反序列化类中的 readExternal,如果不是,则调用 readSerialData() 方法去执行类中的 readObject() 方法。

5.调用invokeReadObject()

readSerialData() 方法中,首先通过类描述符获得了序列化对象的数据布局。通过布局的 hasReadObjectMethod() 方法判断对象是否有重写 readObject 方法,如果有,则使用 invokeReadObject() 方法调用对象中的 readObject

而通过invokeReadObject()方法调用对象中的readObject后,因为我们已经重写了readObject所以导致了其中恶意代码的执行

debug过程中的源码分析

书接上文,在readObject()调用了readObject0()来反序列化字符串后我们来看看它的源码构造

1.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
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}

// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
//从输入流中读取对象
Object obj = readObject0(false);
//标记对象之间的依赖关系
handles.markDependency(outerHandle, passHandle);
//查找可能抛出的异常
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
//如果异常则抛出
throw ex;
}
if (depth == 0) {
//执行回调函数
vlist.doCallbacks();
}
//读取反序列化对象并返回
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}

这段代码是ObjectInputStream的核心方法之一,负责实现对象的反序列化过程,同时处理了一些可能的异常情况和嵌套读取的情况。此时其中的核心是在于Object obj = readObject0(flase),其调用了readObject0()的方法从字节流中读取对象的字节表示,并反序列化为Java对象。

2.readObject0()

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
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}

byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}

depth++;
try {
switch (tc) {
case TC_NULL:
return readNull();

case TC_REFERENCE:
return readHandle(unshared);

case TC_CLASS:
return readClass(unshared);

case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);

case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));

case TC_ARRAY:
return checkResolve(readArray(unshared));

case TC_ENUM:
return checkResolve(readEnum(unshared));

case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));

case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);

case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}

case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
  1. 首先,检查输入流当前是否处于块数据模式(block data mode),如果是,则根据情况抛出OptionalDataException异常或者进行一些处理。
  2. 在一个循环中,不断读取下一个字节并判断其类型(type code)
    • 如果是TC_RESET类型,则调用handleReset()方法进行重置处理。
    • 根据不同的类型码(如TC_NULL、TC_REFERENCE等),调用相应的方法进行对象的读取和处理。
    • 对于一些特殊的类型码,如TC_BLOCKDATA、TC_ENDBLOCKDATA等,根据情况抛出相应的异常。
    • 对于无法识别的类型码,抛出StreamCorruptedException异常。
  3. try-catch-finally块中,处理读取过程中可能出现的异常情况:
    • finally块中,减少深度计数并根据旧的模式恢复块数据模式。

这段代码是ObjectInputStream的一个私有方法,用于具体实现对象的反序列化。此时我们知道readObject0() 方法以字节的方式去读,如果读到 0x73,则代表这是一个对象的序列化数据,将会调用 readOrdinaryObject() 方法进行处理。

3.readOrdinaryObject()

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
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}

ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();

Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}

Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}

passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}

if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}

handles.finish(passHandle);

if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}

return obj;
}

readOrdinaryObject 方法会调用 readClassDesc 方法读取类描述符,并根据其中的内容判断类是否实现了 Externalizable 接口,如果是,则调用 readExternalData 方法去执行反序列化类中的 readExternal,如果不是,则调用 readSerialData 方法去执行类中的 readObject 方法。

4.readSerialDate()

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
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;

if (slots[i].hasData) {
if (obj == null || handles.lookupException(passHandle) != null) {
defaultReadFields(null, slotDesc); // skip field values
} else if (slotDesc.hasReadObjectMethod()) {
SerialCallbackContext oldContext = curContext;
if (oldContext != null)
oldContext.check();
try {
curContext = new SerialCallbackContext(obj, slotDesc);

bin.setBlockDataMode(true);
slotDesc.invokeReadObject(obj, this);
} catch (ClassNotFoundException ex) {
/*
* In most cases, the handle table has already
* propagated a CNFException to passHandle at this
* point; this mark call is included to address cases
* where the custom readObject method has cons'ed and
* thrown a new CNFException of its own.
*/
handles.markException(passHandle, ex);
} finally {
curContext.setUsed();
if (oldContext!= null)
oldContext.check();
curContext = oldContext;
}

/*
* defaultDataEnd may have been set indirectly by custom
* readObject() method when calling defaultReadObject() or
* readFields(); clear it to restore normal read behavior.
*/
defaultDataEnd = false;
} else {
defaultReadFields(obj, slotDesc);
}

if (slotDesc.hasWriteObjectData()) {
skipCustomData();
} else {
bin.setBlockDataMode(false);
}
} else {
if (obj != null &&
slotDesc.hasReadObjectNoDataMethod() &&
handles.lookupException(passHandle) == null)
{
slotDesc.invokeReadObjectNoData(obj);
}
}
}
}

readSerialData 方法中,首先通过类描述符获得了序列化对象的数据布局。通过布局的 hasReadObjectMethod 方法判断对象是否有重写 readObject 方法,如果有,则使用 invokeReadObject 方法调用对象中的 readObject

5.invokeReadObject()

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
void invokeReadObject(Object obj, ObjectInputStream in)
throws ClassNotFoundException, IOException,
UnsupportedOperationException
{
requireInitialized();
if (readObjectMethod != null) {
try {
readObjectMethod.invoke(obj, new Object[]{ in });
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ClassNotFoundException) {
throw (ClassNotFoundException) th;
} else if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}

这段代码也是最终执行恶意代码的地方,这段代码的作用是调用对象的readObject方法,通过反射来实现对对象的readObject方法的调用,并处理可能的异常情况。如果对象没有readObject方法,则抛出UnsupportedOperationException异常。

tips

经过动态调试和源码审计我们已经可以捋出整个反序列化漏洞的形成原因和调用过程,此时的形成原因在于我们重写了对象的readObject()方法,而程序又会调用这个readObject()方法,最终导致了恶意代码的执行。而整个的调用过程如下

1
readObject()  -->  readObject0()  -->  readOrdinaryObject()  -->  readSerialDate()  -->  invokeReadObject()

攻击路线

要满足的一些条件

共同条件:继承Serializable接口

入口类kick-off 或者source;入口点(重写了readObject的类)(重写readObject、参数类型宽泛、调用常见的函数、jdk自带)

调用链gadget chain ;中间的调用链,相同名称、相同类型

执行类sink ;最终执行恶意动作的点,比如exec()

如何找到入口类

HashMap

1.继承Serializable接口

此时最重要的前提是这个类继承了Serializable接口

2.包含重写的readObject()方法

此时我们可以找到HashMap中拥有重写的readObject()

此时我们接着往下看,可以发现参数keyvalue都执行了readObject()方法

执行了readObject()方法之后再将这两个变量扔进hash()函数中;此时我们跟进hash()函数

3.调用常见的函数

此时若满足key不为空,则会继续跳进hashCode()

此时hashCode()位于常见的函数Object类当中,满足调用常见函数。