之前学过php序列化与反序列化,以上就不赘述关于序列化与反序列化的一些概念问题了。感兴趣的师傅可以看看这篇文章:php反序列化 (symya.github.io)
进入正题。

Serializable 接口


基本使用

 只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
 
 Serializable 接口是 Java 提供的序列化接口,它是一个空接口
 Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

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

我们直接来看一个java序列化与反序列化的demo

Person.java文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.Serializable;  

// 调用Serializable接口
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Name:'" + name + "\'" + ", Age:'" + age + "'}";
}
}

serializationTest.java文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.FileOutputStream;  
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class serializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\project\\javasec\\serialize\\test.ser"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException {
Person person = new Person("aa",19);
System.out.println(person);
serialize(person);
}
}

serialize方法用于将传入的对象序列化并写入到指定的文件中。ObjectOutputStream用于将对象的状态转换为字节流,并通过FileOutputStream将这些字节写入到文件test.ser中。
:在main方法中,首先创建了一个Person对象,然后打印出它的字符串表示(调用toString方法),最后将这个对象序列化到文件中。

UnserializationTest.java文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.FileOutputStream;  
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class serializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\project\\javasec\\serialize\\test.ser"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException {
Person person = new Person("aa",19);
System.out.println(person);
serialize(person);
}
}

unserilaize方法用于从文件中读取序列化的对象,并将其反序列化为一个Java对象。ObjectInputStream用于将字节流转换为Java对象。
main方法中,从test.ser文件中读取了序列化的Person对象,进行了类型转换后,打印出反序列化的对象(再次调用toString方法)。

image.png

接口特点

  1. 序列化类的属性没有实现 Serializable 那么在序列化就会报错
  2. 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
  3. 一个实现 Serializable 接口的子类也是可以被序列化的。
  4. 静态成员变量是不能被序列化
    • 序列化是针对对象属性的,而静态成员变量是属于类的。
  5. transient 标识的对象成员变量不参与序列化

关于第五点,还是举上面的例子。我们把Person类中的name属性标识上transient

1
private transient String name;

再次运行序列化和反序列化的代码,得到结果
image.png

  1. Serializable 在序列化和反序列化过程中大量使用了反射,因此其过程会产生的大量的内存碎片

关于readObject和writeObject

jdk中默认提供,也可重写,开发中重写多是为了避免资源浪费,但引入了安全问题。原因是只要服务端反序列化数据,客户端传递类的readObject中的代码会自动执行。

可能可以利用的形式:

  • 入口类参数中包含可控类,可控类调用其他危险函数的类,readObject时调用
  • 构造函数/静态代码块等类加载时隐式执行。

关于第一种可利用的形式,我们还是看上面那个例子。在理想条件下:
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.IOException;  
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

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

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

@Override
public String toString() {
return "{Name:'" + name + "\'" + ", Age:" + age + "}";
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}

序列化与反序列化后就会发现,重写的readObject方法导致了命令执行。

条件:

  1. 继承或使用seriliable(必需要的)
  2. 入口类source –> map类,hashMap等
    1. 重写readObject()
    2. 参数类型宽泛 ,调用的常见的函数
    3. 最好是jdk自带的
  3. 调用链 gadget chain
    • 相同名称 相同类型 不同的调用
  4. 找到一个执行类 sink (rce ssrf 写文件等等)

参考文章:
Drun1baby/JavaSecurityLearning: 记录一下 Java 安全学习历程,也算是半条学习路线了 (github.com)
java序列化与反序列化全讲解_反序列化会进无参构造吗-CSDN博客
Java反序列化漏洞专题-基础篇(21/09/05更新类加载部分)_哔哩哔哩_bilibili