logo头像

黑客的本质就是白嫖

Java序列化与反序列化基础

前言

因为谁都说我菜,让我学东西,而我的Java基础又比较差,就自然而然地拿来了做开胃菜并不

Java里面个人认为很重要的一个东西就是其序列化与反序列化,因为万物皆对象嘛(我突然感觉Java程序员才是怨念最深的存在,连个编程语言都不放过),而要存储对象,按照PHP里面的做法就是把对象的各种数据转化成一串字符串,里面包含了变量类型、变量名、变量值等等,Java的话,感觉也应该是这样的吧,嗯,感觉

正文

PHP中,反序列化漏洞一般出现在__destruct等魔术函数中,而Java的话,我们先研究一下Java的序列化吧

原生JDK库中有两个API是对序列化与反序列化进行操作的

java.io.ObjectOutputStream 对象输出流

java.io.ObjectInputStream 对象输入流

对象输出流中的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,并把得到的字节序列写入到一个目标输出流当中;

对象输入流则相反,它当中的readObject()方法从一个源输入流中读取一串字节序列,再将其反序列化为一个对象,并返回

只有实现了SerializableExternalizable接口的类的对象才能被序列化,Externalizable接口继承自Serializable,换句话说,被序列化的对象的类一定要继承自这两个接口,实现了Externalizable接口的类完全由自身来控制序列化行为,而仅实现了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
package com.dldxz.study;

import java.io.Serializable;

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

public int getAge() {
return age;
}

public String getName() {
return name;
}

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

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

@Override
public String toString() {
return "{\"age\":"+this.age+",\"name\":"+this.name+"}";
}
}

以上定义的是一个java类,重写了toString()方法,确保在输出的时候可以输出其变量,下面是进行序列化的类

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

import java.io.*;

public class TestSerialize {
public static void main(String[] args) throws Exception {
SerializePerson();
Person p = DeserializePerson();
System.out.println("p:"+p.toString());
}

private static void SerializePerson() throws FileNotFoundException, IOException {
Person p1 = new Person();
p1.setAge(10);
p1.setName("dldxz");

ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(new File("D:\\Program\\IdeaProjects\\Study\\Person.txt")));
os.writeObject(p1);
System.out.println("p1:"+p1.toString());
os.close();
}

private static Person DeserializePerson() throws Exception,IOException {
ObjectInputStream oi = new ObjectInputStream(new FileInputStream(new File("D:\\Program\\IdeaProjects\\Study\\Person.txt")));
Person p2 = (Person) oi.readObject();
System.out.println("p2:"+p2.toString());
return p2;
}
}

里面调用了序列化、反序列化方法,每个方法里面都有一个不同的输出确认程序运行顺序

下面是运行时的输出

1
2
3
p1:{"age":10,"name":dldxz}
p2:{"age":10,"name":dldxz}
p:{"age":10,"name":dldxz}

嗯,和预期输出没有多大的出入,但是写到这里我感觉Java的序列化虽然也叫序列化,但和php的序列化明显不同,php序列化之后就是一串可见的字符串,可以直接将其print出来,而Java的序列化是保存在一个文件里面,好像还是按照字节保存的,等我到现场的时候,就只看到了下面的场景

fxlh_1

而直接用文本编辑器打开txt文件,看到的却又是似是乱码而又有明文的字符串

fxlh_2

嘛尝试动态调试了一下发现不是很能看得懂他的序列化算法,就当成黑盒来用吧

无论是PHP还是Java,序列化和反序列化的过程个人认为都是一个动态和静态相互转换的过程,但是一个静态的东西怎么转化成动态呢?从哲学的角度来看,静态的事物所包含的信息量是远远比不上动态的事物的,一本牛津字典里面的信息量可能也没有一个小虫子身上的信息量大……emmmm扯远了,其实这是因为看到了一个在序列化与反序列化里要用到的东西,叫serialVersionUID

从字面上理解的话,序列化版本UID?好像和本身用处差距也不是很大,这个UID就是用来标识序列化的类的,程序员考虑到一个对象被序列化之后,其原本的类的代码有可能会进行修改,之后再反序列化就找不到它当时那个一模一样的类了,而这个UID的作用就是让修改后的类和序列化的对象兼容,让其能够反序列化回来,嗯,就是身份证嘛

1
2
3
4
5
6
7
8
9
10
11
12
package com.dldxz.study;

import java.io.*;

public class TestSerialize {
public static void main(String[] args) throws Exception {
SerializePerson();
//Person p = DeserializePerson();
//System.out.println("p:"+p.toString());
}
...
}

首先序列化

1
p1:{"age":10,"name":dldxz}

很正常,接下来修改Person类的方法,添加一个sex变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.dldxz.study;

import java.io.Serializable;

public class Person implements Serializable {
private int age;
private String name;
private String sex;
...
public String getSex() {
return sex;
}
...
public void setSex(String sex) {
this.sex = sex;
}
...
}

尝试反序列化

1
2
3
4
5
6
7
8
9
10
11
12
package com.dldxz.study;

import java.io.*;

public class TestSerialize {
public static void main(String[] args) throws Exception {
//SerializePerson();
Person p = DeserializePerson();
System.out.println("p:"+p.toString());
}
...
}

果然报错了

1
2
3
4
5
6
7
8
9
Exception in thread "main" java.io.InvalidClassException: com.dldxz.study.Person; local class incompatible: stream classdesc serialVersionUID = 8601589671952285069, local class serialVersionUID = 2810157375773054337
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.dldxz.study.TestSerialize.DeserializePerson(TestSerialize.java:25)
at com.dldxz.study.TestSerialize.main(TestSerialize.java:8)

那么我们自己添加一个UID上去呢

1
2
p2:{"age":10,"name":dldxz,"sex":null}
p:{"age":10,"name":dldxz,"sex":null}

能够正常序列化,只不过多出来的那个变量值由于没有设置是null

还有一个不能序列化的情况

  • 如果该类的某个属性标识为static类型的,则该属性不能序列化
  • 如果该类的某个属性采用transient关键字标识,则该属性不能序列化
  • 如果该类有父类,则分两种情况来考虑:如果该父类已经实现了可序列化接口,则其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值

好吧我尝试了一下不知道是方法不对还是什么情况,还是能序列化成功,日后有时间的时候再研究一下这个问题

References

Java 序列化 | 菜鸟教程

Java基础学习总结——Java对象的序列化和反序列化

Java反序列化基础

评论系统未开启,无法评论!