1、序列化与反序列化(见图)
序列化:将内存中的对象压缩成字节流
反序列化:将字节流转化成内存中的对象
序列化与反序列化其实就是对象与数据格式的转换。
序列化与反序列化的设计就是用来传输数据的。
当两个进程进行通信的时候,可以通过序列化反序列化来进行传输。
能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。
应用场景:
(1) 想把内存中的对象保存到一个文件中或者是数据库当中。
(2) 用套接字在网络上传输对象。
(3) 通过RMI传输对象的时候。
• JAVA内置的writeObject()/readObject()
• JAVA内置的XMLDecoder()/XMLEncoder
• XStream
• SnakeYaml
• FastJson
• Jackson
(1) 可控的输入变量进行了反序列化操作
(2) 实现了Serializable或者Externalizable接口的类的对象
(3) 能找到调用方法的危险代码或间接的利用链引发(依赖链)
本文以JAVA内置的 writeObject()
/ readObject()
为参考
//User.java
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
public String name;
public int age;
public String gender;
public String address;
public User(String name, int age, String gender, String address) {
this.name = name;
this.age = age;
this.gender = gender;
this.address = address;
}
@Override
public String toString() {
return name + " " + age + " " + gender + " " + address;
}
}
//SerializableTest.java
import java.io.*;
public class SerializableTest {
public static void main(String[] args) throws IOException {
// 创建User对象并序列化
User user = new User("c1trus", 18, "man", "ChongQing");
SerializableTest(user);
}
public static void SerializableTest(Object obj) throws IOException {
//FileOutputStream()输出文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
//将对象Obj写入到文件 ser.txt
oos.writeObject(obj);
}
}
运行后就会获取到一个 ser.txt
序列化数据
JAVA内置的writeObject()/readObject()内置原生写法分析:
//UnSerializable.java
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnSerializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Object o =UnserializableTest("ser.txt");
System.out.println(o);
}
public static Object UnserializableTest(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object o= ois.readObject();
return o;
}
}
反序列化后就能把序列化数据 ser.txt
还原数据
我们在User类中重写 readObject
方法 里面加上危险的代码
private void readObject (java.io.ObjectInputStream stream) throws IOException {
System.out.println("这是一个危险的方法");
Runtime.getRuntime().exec("calc");
}
我们先把User对象 进行序列化,然后在反序列化,成功触发了危险代码
在User
类中,攻击者重写了readObject()
方法
序列化的时候将User
对象正常写入文件ser.txt
,然后反序列化将ser.txt
进行反序列化。
然后执行ois.readObject()
,因为ois
就是user
对象,所以执行的就是user.readObject()
只要序列化对象被输出,那么就会调用 如果 toString
方法,toString
方法里面含有危险代码,那么就可以被利用
这里我们给 user
类的 toString
方法加上一些危险代码
public String toString() {
System.out.println("这是toString方法");
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
return "";
}
然后进行序列化,再反序列化,成功触发危险代码
如果序列化的对象写死了。但是反序列化的二进制数据流可以被我们控制,那么就可以反序列化我们任意的类 (这里我用的是自己写的Calc
类)
而这些类就包括java自带的,还有组件中的。 组件中的我们就可以通过反射 代理进行调用,这就是链的组成
我们先创建一个 Clac
类,里面写入危险代码
//calc.java
import java.io.IOException;
public class Calc implements java.io.Serializable {
private void readObject(java.io.ObjectInputStream in) throws IOException {
System.out.println("Calc readObject");
Runtime.getRuntime().exec("calc");
}
}
然后对其进行序列化为 sercalc.txt
如果此时我们可以控制反序列化的对象 从 ser.txt
-> sercalc.txt
那么我们就会触发 Calc
类中的危险代码
入口类的readObject直接调用危险方法
(2) 入口参数中包含可控类,该类有危险方法,readObject时调用
(3) 入口类参数包含可控类,该类又调用其他有危险方法类,readObject调用
(4) 构造函数/静态代码块等类加载时隐式执行