初探java反序列化

Java反序列化

理解Java序列化和反序列化

Serialization(序列化):将java对象以一连串的字节保存在磁盘文件中的过程,也可以说是保存java对象状态的过程。序列化可以将数据永久保存在磁盘上(通常保存在文件中)。

deserialization(反序列化):将保存在磁盘文件中的java字节码重新转换成java对象称为反序列化。

序列化和反序列化的应用

当我们需要将内存中的对象信息存储到磁盘或者通过网络发送到第三者,这个时候就需要通过序列化和反序列化的方式去实现 发送的时候将java对象通过序列化转化为字节序列,然后在网络上传输,接收方收到后进行反序列化把字节序列转成java对象

序列化和反序列化地实现

JDK类库提供的序列化API:

  • java.io.ObjectOutputStream
    表示对象输出流,其中writeObject(Object obj)方法可以将给定参数的obj对象进行序列化,将转换的一连串的字节序列写到指定的目标输出流中。
  • java.io.ObjectInputStream
    该类表示对象输入流,该类下的readObject(Object obj)方法会从源输入流中读取字节序列,并将它反序列化为一个java对象并返回。

序列化要求:

实现序列化的类对象必须实现了Serializable类或Externalizable类才能被序列化,否则会抛出异常。

实现java序列化和反序列化的三种方法:

现在要对student类进行序列化和反序列化,遵循以下方法:

方法一:若student类实现了serializable接口,则可以通过objectOutputstream和objectinputstream默认的序列化和反序列化方式,对非transient的实例变量进行序列化和反序列化。

方法二:若student类实现了serializable接口,并且定义了writeObject(objectOutputStream out)和

readObject(objectinputStream in)方法,则可以直接调用student类的两种方法进行序列化和反序列化。

方法三:若student类实现了Externalizable接口,则必须实现readExternal(Objectinput in)和writeExternal(Objectoutput out)方法进行序列化和反序列化。

demo

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 com.test;

import java.io.*;

public class StudentUnSerializableDemo1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Student test = new Student("test", '1', 18);
File file = new File("F://JAVA学习//javaSecurityStudy//student.txt");
if(file.exists()){
System.out.println("文件存在");
}else{
file.createNewFile();
}
//Student对象序列化过程
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
//调用 ObjectOutputStream 中的 writeObject() 方法 写对象
oos.writeObject(test);
oos.flush();
oos.close();
fos.close();

//Student对象反序列化过程
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Student st1 = (Student)ois.readObject();
ois.close();
fis.close();
System.out.println(st1);
}
}

image-20221107164109991

java反序列化的特征

image-20221107164426571

这里是用010edit打开的

transient关键字

被transient修饰的数据不能进行序列化

demo

image-20221107164630194

image-20221107164653015

然后用之前的测试代码可以发现sex已经看不到值了

Externalizable接口实现序列化与反序列化

Externalizable接口继承Serializable接口,实现Externalizable接口需要实现readExternal()方法和writeExternal()方法,这两个方法是抽象方法,对应的是serializable接口的readObject()方法和writeObject()方法,可以理解为把serializable的两个方法抽象出来。Externalizable没有serializable的限制,static和transient关键字修饰的属性也能进行序列化。

我们将之前的student类复制一份

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
package com.test;

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


public class Student1 implements Externalizable {
//私有化成员变量
private String name;
private transient char sex;
private int age;


public Student1(){ //无参构造
}
public Student1(String name, char sex, int age){
//参数给属性赋值
this.name = name;
this.sex = sex;
this.age = age;

}
//重写set和get
public String getName() {
return name;
}

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

public char getSex() {
return sex;
}

public void setSex(char sex) {
this.sex = sex;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '/'' +
", sex=" + sex +
", age=" + age +
'}';
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeObject(sex);
out.writeObject(age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
sex = (char) in.readObject();
age = (int) in.readObject();
}
}

使之实现Externalizable然后重写方法writeExternal,readExternal

测试代码

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 com.test;

import java.io.*;

public class StudentUnSerializableDemo1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// Student test = new Student("test", '1', 18);
Student1 test = new Student1("test1", '1', 181);
File file = new File("F://JAVA学习//javaSecurityStudy//student1.txt");
if(file.exists()){
System.out.println("文件存在");
}else{
file.createNewFile();
}
//Student对象序列化过程
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
//调用 ObjectOutputStream 中的 writeObject() 方法 写对象
oos.writeObject(test);
oos.flush();
oos.close();
fos.close();

//Student对象反序列化过程
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Student1 st1 = (Student1)ois.readObject();
ois.close();
fis.close();
System.out.println(st1);
}
}

image-20221107170548907

可以看到sex是被反序列化出来了的

介绍完了序列化和反序列化 下面看看基础漏洞

漏洞demo

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
package com.test;

import java.io.IOException;
import java.io.Serializable;

public class Student3 implements Serializable {
//私有化成员变量
private String name;
private char sex;
private int age;


public Student3(){ //无参构造
}
public Student3(String name, char sex, int age){
//参数给属性赋值
this.name = name;
this.sex = sex;
this.age = age;

}
//重写set和get
public String getName() {
return name;
}

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

public char getSex() {
return sex;
}

public void setSex(char sex) {
this.sex = sex;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '/'' +
", sex=" + sex +
", age=" + age +
'}';
}

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

漏洞利用

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
package com.test;

import java.io.*;

public class StudentUnSerializableDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
Serilizable();
//反序列化
UnSerializable();


}
public static void Serilizable() throws IOException {
Student3 test3 = new Student3("test3", '1', 19);
File file = new File("F://JAVA学习//javaSecurityStudy//student3.txt");
if (file.exists()){
System.out.println("文件存在");
}else {
file.createNewFile();
}

try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(test3);
System.out.println("serilizable: " + test3);
oos.flush();
oos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}


}

public static Object UnSerializable() throws IOException, ClassNotFoundException {
File file = new File("F://JAVA学习//javaSecurityStudy//student3.txt");
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Student3 student3 = (Student3) ois.readObject();
System.out.println("unserializable: " + student3);
ois.close();
return student3;
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}

}
}

运行

image-20221107180406030

漏洞成因分析

在student3类里面 我们重写的readObject函数 并在里面加入了危险代码Runtime.getRuntime().exec(“calc.exe”)

然后在反序列化的时候会执行我们的危险代码,虽然这里简单粗暴的将执行命令的代码写入了方法。实际的情况下,开发人员是不会这么做。实际情况都是攻击者通过各种伪造方法、修改、重定义等方法最后到达执行命令。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!