学无止境,参考着资料边学边记录吧。

0x01 Java序列化与反序列化

Java序列化是指把Java对象转换为字节序列的过程。这一过程将数据分解成字节流,以便存储在文件中或在网络上传输;

Java反序列化是指把字节序列恢复为Java对象的过程。就是打开字节流并重构成对象,恢复数据。

序列化与反序列化都可以理解为“写”和“读”操作 ,通过以下这两个方法可以将对象实例进行“序列化”与“反序列化”操作。

1
2
3
4
5
// 写入对象内容
private void writeObject(java.io.ObjectOutputStream out)

// 读取对象内容
private void readObject(java.io.ObjectInputStream in)

0x02 为什么需要序列化与反序列化

当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。而当两个Java进程进行通信时,可以通过Java的序列化与反序列化在进程之间直接传送对象,换句话说,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;接收方需要从字节序列中恢复出Java对象。

使用场景:

  • 想把内存中的对象保存到一个文件中或者数据库中时候;

  • 想用套接字在网络上传送对象的时候;

  • 想通过RMI传输对象的时候

一些应用场景涉及到将对象转化成二进制,序列化保证了能够成功读取到保存的对象。

总之,序列化的用途就是传递和存储。

0x03 序列化实现的方式

3.1 Serializable

将要序列化的类实现 Serializabel 接口(Serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,则表明该类的对象就是可序列化的),而且所有属性必须是可序列化的,就是如果一个可序列化的类的成员不是基本类型,也不是String类型,比如自己自定义的类,那这个引用类型也必须是可序列化的,否则,会导致此类不能序列化(用transient关键字修饰的属性除外,不参与序列化过程) 。

需要序列化的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package yhy;
/**
* @author yhy
* @date 2021/4/4 21:46
* @github https://github.com/yhy0
*/
import java.io.Serializable;


public class User implements Serializable {
private String name;

public String getName() {
return name;
}

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

序列化

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
package yhy;
/**
* @author yhy
* @date 2021/4/4 22:14
* @github https://github.com/yhy0
*/
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

// 序列化和反序列化
public class UserSerializable {

public static void main(String[] args) throws Exception {
User user = new User();
user.setName("yhy");

// 序列化, 将对象转化为字节序列
serialize(user);

}

public static void serialize(User user) throws Exception {
FileOutputStream fout = new FileOutputStream("user.ser");
ObjectOutputStream out = new ObjectOutputStream(fout);
out.writeObject(user);
out.close();
fout.close();
System.out.println("序列化完成.");
}

}

可以看到运行后,生产了一个文件,将user 对象变成了可持久化存储的二进制数据。

image-20210404222530833

可以来看一下该对象序列化后的二进制数据

image-20210404223840477

序列化的数据流以魔术数字和版本号开头,这个值是在调用ObjectOutputStream序列化时,由writeStreamHeader方法写入。开头的几位一般来当作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
27
28
29
30
31
32
33
34
35
36
37
38
package yhy;

/**
* @author yhy
* @date 2021/4/4 22:14
* @github https://github.com/yhy0
*/

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

// 序列化和反序列化
public class UserSerializable {

public static void main(String[] args) throws Exception {
User user = new User();
user.setName("yhy");

// 序列化, 将对象转化为字节序列
// serialize(user);
// 反序列化,将字节序列恢复为对象
User user1 = unserialize();
System.out.println(user1.getName());

}

public static User unserialize() throws Exception {
FileInputStream fileIn = new FileInputStream("user.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
User user = (User) in.readObject();
in.close();
fileIn.close();
return user;
}
}

image-20210404224748419

读取了序列化文件,将二进制文件重新恢复为user对象,对象里面的属性也是完美恢复。

3.2 Externalizable

通过实现Externalizable接口进行序列化和反序列胡,但必须实现writeExternalreadExternal方法,并且还要实现一个类的无参构造方法Serializable 接口可以不用实现。

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
package yhy;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

/**
* @author yhy
* @date 2021/4/5 00:24
* @github https://github.com/yhy0
*/

public class Evil implements Externalizable {

// 实现了Externalizable这个接口需要提供无参构造,在反序列化时会检测
public Evil() {
System.out.println(this.getClass() + "的EvilClass()无参构造方法被调用!!!!!!");
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {

}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

}

}

用法和实现了Serializable接口一样,这里就不演示了。

0x04 readObject()方法

特地提到这个方法是因为在反序列化漏洞中它起到了关键作用。因为在序列化过程中,JVM虚拟机会试图调用对象类里的 writeObjectreadObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStreamdefaultWriteObject 方法以及 ObjectInputStreamdefaultReadObject 方法。用户自定义的 writeObjectreadObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

Java反序列化的过程中可以自动执行序列化类的四个方法,实现了Serializable接口可以执行的方法包括readObjectreadObjectNoDatareadResolve,以及实现了Externalizable接口的readExternal方法。这些在找反序列化漏洞时都需要重点关注。

如果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
51
52
package yhy;

/**
* @author yhy
* @date 2021/4/4 23:34
* @github https://github.com/yhy0
*/

import java.io.ObjectInputStream;
import java.io.Serializable;

public class EvilClass implements Serializable {
String name;

public EvilClass() {
System.out.println(this.getClass() + "的EvilClass()无参构造方法被调用!!!!!!");
}

public EvilClass(String name) {
System.out.println(this.getClass() + "的EvilClass(String name)构造方法被调用!!!!!!");
this.name = name;
}

public String getName() {
System.out.println(this.getClass() + "的getName被调用!!!!!!");
return name;
}

public void setName(String name) {
System.out.println(this.getClass() + "的setName被调用!!!!!!");
this.name = name;
}

@Override
public String toString() {
System.out.println(this.getClass() + "的toString()被调用!!!!!!");
return "EvilClass{" +
"name='" + getName() + '\'' +
'}';
}


private void readObject(ObjectInputStream in) throws Exception {
//执行默认的readObject()方法
in.defaultReadObject();
System.out.println(this.getClass() + "的readObject()被调用!!!!!!");
// windows
// Runtime.getRuntime().exec(new String[]{"cmd", "/c", name});
// mac
Runtime.getRuntime().exec(new String[]{"open", "-a", name});
}
}

readObject中存在执行命令的代码Runtime.getRuntime().exec(new String[]{"open", "-a", name}),name参数是要执行的命令。那么我们可以构造一个恶意的对象,将其name属性赋值为要执行的命令,当反序列化触发readObject时就会RCE。如下

序列化和反序列化

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
package yhy;

import java.io.*;

/**
* @author yhy
* @date 2021/4/4 23:36
* @github https://github.com/yhy0
*/


public class EvilSerialize {
public static void main(String[] args) throws Exception {
EvilClass evilObj = new EvilClass();
// evilObj.setName("calc");
// mac
evilObj.setName("Calculator");

// 序列化为字节数组
byte[] bytes = serializeToBytes(evilObj);
// 反序列化
EvilClass o = (EvilClass)deserializeFromBytes(bytes);
System.out.println(o);
}

public static byte[] serializeToBytes(final Object obj) throws Exception {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
objOut.flush();
objOut.close();
return out.toByteArray();
}

public static Object deserializeFromBytes(final byte[] serialized) throws Exception {
final ByteArrayInputStream in = new ByteArrayInputStream(serialized);
final ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
}

image-20210405000541157

这是一个极端的例子,在真实场景中,不会有人真的这样直接写一句执行命令的代码在readObject()中,这样写的开发绝对会被拉出去祭天的。所以反序列化漏洞通常会需要Java的一些特性进行配合比如反射(invoke)。然后就是利用链的寻找。反序列化漏洞需要三个东西

  1. 反序列化入口(source)
  2. 目标方法(sink)
  3. 利用链(gadget chain)

大佬们基本都会去寻找重写了这个readObject方法的类,并配合Java的invoke反射机制,构造利用链,形成了Java中最具特色的反序列化攻击。而且再看上图中的输出结果,不仅仅触发了readObject方法,还触发了toString()无参构造setget方法,那么在实际寻找利用链的过程中就不仅仅需要关注readObject()的方法了。

代码地址:https://github.com/yhy0/JavaSerializeDemo

0x05 参考

Java反序列之从萌新到菜鸟 https://www.kingkk.com/2019/01/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E4%B9%8B%E4%BB%8E%E8%90%8C%E6%96%B0%E5%88%B0%E8%8F%9C%E9%B8%9F/

Java反序列化技术分享 https://github.com/Y4er/WebLogic-Shiro-shell