Java反序列化漏洞之Java反序列化流程与分析(3)
2023-06-15 17:11:05 # Web Security # Java Deserialization

什么是Java序列化和反序列化?

在Java中,序列化是为了方便传输存储数据的一种方式。

If we want to transfer an object, for instance, store it on a disk or send it over a network, we need to transform it into a byte stream.

Java序列化会将一个object转换成byte[],也就是一个含有object状态(属性信息)的二进制数组。Java序列化会使用Java反射来获取到需要被序列化的数据,包括private和final的fields。常用writeObject()来序列化Object。

Java serialization uses reflection to scrape all the data from the object’s fields that need to be serialized. This includes private and final fields

Java反序列化会根据该二进制流,重新创建一个相同状态下的object。Java反序列化不会用constructor来创建一个object,相反,他会创建一个空的object,然后用Java反射把数据写进属性里,所以在重新创建object的时候,constructor里的代码是不会被执行的(除了实现Externalizable接口的Class,之后会提到)。常用readObject()来反序列化Object。

When deserializing a byte stream back to an object it does not use the constructor. It creates an empty object and uses reflection to write the data to the fields.

显而易见,Java在序列化和反序列化的过程中,都用到了Java反射机制(需要注意的是,实现Externalizable接口并不会使用Java反射机制,这一点会在后面的内容中讲到),而整个过程其实就是数据转换为二进制流,再根据数据重新创建一个相同的object。

Java序列化和反序列的实现

Serializable接口

介绍

如果一个Class想要被序列化,那么他必须implements Serializable

The serialization interface has no methods or fields and serves only to identify the semantics of being serializable.

Serializable接口没有任何的方法或属性,它只是用来标识该Class是否可以被序列化。

根据官方注解,序列化的Class会有一个serialVersionUID,它会在反序列化的时候,被用来验证发送者和接收者是否有一样的serialVersionUID。需要注意的是,我们所声明的serialVersionUID,必须为 static final long serialVersionUID

如果不声明serialVersionUID,Java在序列化的时候会计算默认的serialVersionUID值,但官方强烈建议所有Serializable Class都声明该值,避免一些不必要的错误。

编写Serializable Class

1
2
3
4
5
6
7
8
9
public class Student implements Serializable {
static final long serialVersionUID =1L;

public String name = "shulei";
public void hello(){
System.out.println("hello"+name);
}

}

需要被序列化的Class,都要实现Serializable接口。

编写实现序列化与反序列化Class

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 SerializableTest {
public static void main(String[] args) throws Exception {

Student student=new Student();//create a Student instance
serializeObj(student);
deserializeObj();
}
public static void serializeObj(Student student) throws Exception {//序列化
FileOutputStream fileOutputStream = new FileOutputStream("test.cer");//创建一个文件输出流,文件名为test.cer
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);//创建一个对象输出流,对象数据流中的数据将输出到文件输出流中
objectOutputStream.writeObject(student);//将student这个object输入到文件输出流中
objectOutputStream.close();//关闭文件输出流和对象输出流,避免内存泄露
fileOutputStream.close();
//序列化完成
}
public static void deserializeObj() throws Exception{
FileInputStream fileInputStream = new FileInputStream("test.cer");//创建文件输入流,读取test.cer
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);//创建对象输入流,将文件输入流里的数据输入对象输入流
Student student =(Student)objectInputStream.readObject();//读取输入流中的对象,强制转换为Student类型,重构对象
student.hello();//call方法
//反序列化完成
}
}

运行测试

image-20210728193330087

Externalizable接口

介绍

除了通过implements Serializable来让Class可序列化,我们同样可以使用Externalizable来标识Class是可序列化的。

进入Externalizable interface可以看到以下结构:

1
2
3
4
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

实际上,Externalizable是继承了Serializable,这也就能说明为什么它也能够用来表示Class是可序列化的了。但不同的是,这里有两个额外的method,writeExternal()readExternal()

所以当我们实现Externalizable接口的时候,我们需要重写这两个method。

writeExternal()readExternal()分别替代了writeObject()readObject()两个methods,开发者需要手动对数据进行序列化和反序列化,这意味着,我们可以选择性地序列化和反序列化某些属性,相比Serializable接口就更加的灵活了。

此外,实现Externalizable的Class必须要有默认的无参构造函数,因为Externalizable在反序列化时不使用反射机制,所以它必须要有constructor。采用Externalizable无需产生serialVersionUID,而Serializable接口需要。

image-20210728231725366

编写Externalizable Class

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
public class XiaoXueSheng  implements Externalizable {
private String name;
private int age;
public XiaoXueSheng() {//此无参数的构造方法必须存在
}
public XiaoXueSheng(String name,int age){
this.name=name;
this.age=age;
}

public int getAge() {
return age;
}

public String getName() {
return name;
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);//将name写入对象输出流和文件输出流

}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
String name = (String)in.readObject();//从对象输入流中获取name
this.name=name;
}
}

我们在writeExternal()方法中写入name属性,并在readExternal()中取出赋值给当前Class的name属性。相反另一个属性age并没有被我们序列化。

那么可以思考一下,当我们反序列化后,我们得到的age是什么值呢?

编写实现序列化和反序列化Class

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
public class SerializableTest {
public static void main(String[] args) throws Exception {

XiaoXueSheng xiaoXueSheng = new XiaoXueSheng("Leihehe",21);
//我们创建一个有name和age的值的object
serializeObj(xiaoXueSheng);//序列化该object
deserializeObj();//反序列化该object
}
public static void serializeObj(XiaoXueSheng xiaoXueSheng) throws Exception {
FileOutputStream fileOutputStream = new FileOutputStream("test.cer");//创建一个文件输出流,文件名为test.cer
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);//创建一个对象输出流,对象数据流中的数据将输出到文件输出流中
objectOutputStream.writeObject(xiaoXueSheng);//将xiaoXueSheng这个object输入到文件输出流中
objectOutputStream.close();//关闭文件输出流和对象输出流,避免内存泄露
fileOutputStream.close();
}

public static void deserializeObj() throws Exception{
FileInputStream fileInputStream = new FileInputStream("test.cer");//创建文件输入流,读取test.cer
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);//创建对象输入流,将文件输入流里的数据输入对象输入流
XiaoXueSheng xiaoXueSheng=(XiaoXueSheng) objectInputStream.readObject();//重构object,将反序列化得到的数据重新赋值到新的object中
System.out.println(xiaoXueSheng.getName());
System.out.println(xiaoXueSheng.getAge());
}

}

我们创建了一个name为Leihehe, age为21的object,将它进行序列化和反序列化。

在反序列化操作后,我们将object中的name和age输出。

运行测试

image-20210728200635043

我们发现name被正常输出了,但age为initilised value,这就解答了之前的问题。

在可序列化Class XiaoxueSheng中,我们只规定将name序列化和反序列化,并未序列化age,所以当反序列化创建新的object后(执行无参constructor),age并未被赋值,而是最初的状态。

Java反序列化漏洞

什么是反序列化漏洞?

A Java deserialize vulnerability is a security vulnerability that occurs when a malicious user tries to insert a modified serialized object into the system that eventually compromises the system or its data.

敲重点:Java反序列化了被恶意修改序列化对象(须是服务器中存在的对象或者依赖包中的对象)。

案例一

根据Java官方说明,任何实现Serializable接口的Class都可以定义自己的readObject()方法,只要在重写方法的同时执行了defaultReadObject()方法即可。这样在反序列化的时候会自动invoke该Class下自己定义的readObject()方法。

那么我们可以为Class重写一个它自己的readObject()的方法,里面带有恶意执行代码,让Java去反序列化这个我们修改后的Object,这样readObject()被执行的时候,恶意代码也就被执行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Student implements Serializable {
static final long serialVersionUID =1L;

public String name = "shulei";
public void hello(){
System.out.println("hello"+name);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();

Process p =Runtime.getRuntime().exec("ipconfig");//执行ipconfig命令

//将命令结果打出来
BufferedInputStream bfIn = new BufferedInputStream(p.getInputStream());
BufferedReader br = new BufferedReader(new InputStreamReader(bfIn));
String s=null;
while ((s = br.readLine()) != null) {
System.out.println(s);
}

}
}

此处我们重写了readObject(),其中含有命令执行代码。

重新反序列化后,发现命令被执行。

image-20210728203543869

案例二

我们序列化的时候,将数据存放进了一个叫test.cer的文件中,当我们把这个序列化后的数据修改一下,那么效果也是一样的。

这里我用Notepad++打开,再使用它的HEX-Editor插件,可以看到该文件十六进制的格式。我们将此处修改一下

image-20210728204023096

image-20210728204136266

我们只让它反序列化我们修改好的object

image-20210728204313877

内容已经被恶意修改了。

序列化后的数据分析

在前一部分的案例二中,我们在十六进制文件的基础上修改了数据,那么其中数据到底有什么意义呢?

这里我将用到SerializationDumper来分析。将HEX的值复制粘贴到这个项目中即可(不知为何,在NodePad++中直接复制粘贴,00会变成20,需要替换回来)。

image-20210728205742670

这样一个清晰的结构对我们了解序列化的数据储存结构很有帮助。

  1. ACED0005

0xac ed是Java序列化的字符串魔术数,相当于是Java序列化的十六进制特征码,看到这个值我们就能知道这是Java序列化后的十六进制的值

0x00 05是JAVA序列化的版本号

  1. 7372

0x73TC_OBJECT, 代表下面的内容是一个新的对象

0x72TC_CLASSDESC,Class描述符,代表下面是一个新的Class

0x00 07 代表 Class Name长度

0x53747564656e74代表 Class Name - Student

0x00 00 00 00 00 00 00 01代表serialVersionUID的值

后面的就不说了,具体资料可以在网上查到,配合SerializationDumper来学习分析会很有帮助。

Reference

Serialization and deserialization in Java: explaining the Java deserialize vulnerability

JAVA 对象序列化(二)——Externalizable

java中Serializable和Externalizable接口浅析

Java 反序列化漏洞(3) – 初探 Java 反序列化漏洞以及序列化数据分析