原理
什么是序列化和反序列化
所谓序列化,就是指将内存中的某个对象压缩成字节流的形式,而反序列化,则是将字节流转化成内存中的对象。
反序列化本身是为了还原对象本身,不存在安全问题,但问题在于不可控的在还原对象时候产生了“额外的操作”
java反序列化和其他语言反序列化的区别
python反序列化漏洞可以用__reduce__魔术方法来重写里面的方法来达到我们的目的,php反序列化也同样是靠触发魔术方法,而java反序列化本身不依赖这些额外函数的调用,它是在还原这个对象本身的时候触发了多余的调用(比如还原HashMap).
"java反序列化触发的逻辑会因为链条的不同而产生不同的差异"
最主要的差异在于python和php的反序列化漏洞是开发者自己写出来的,但是java很多时候开发者不知道他用的一些组件(比如hashmap)的底层的反序列化是怎么实现的,无意之中就误打误撞写出来一个反序列化.java反序列化更多的是基础组件不可控导致的
java序列化和反序列化
序列化
在Java当中,如果一个类需要被序列化和反序列化,需要实现java.io.Serializable接口(其作用只是做一个类型判断,不需要序列化的类就可以不用序列化),并调用ObjectOutputStream类的writeObject方法即可.
在序列化的过程中,是针对对象本身,并不针对类.因此静态属性(static)是不参与序列化和反序列化的过程的。另外,如果属性本身声明了transient关键字,也会被忽略。但是如果某对象继承了A类,那么A类当中的对象的对象属性也是会被序列化和反序列化的(前提是A类也实现了java.io.Serializable接口).
如果继承的父类没有实现java.io.Serializable接口,将不会反序列化
反序列化
序列化使用ObjectOutputStream类的writeObject,反序列化使用的则是ObjectInputStream类的readObject方法,readObject函数返回的是Object类型的对象,因此需要做强制的类型转换
具体实现就是序列化的逆过程,会根据序列化读出数据的类型,进行相应的处理,比如是Class,则会调用Class.forName反射获取对应的类信息
(如图)
在反序列化对象的时候,跟进readObject,在readObject0中,核心处理类的方法是readOrdinaryObject
(readOrdinaryObject函数负责将非特定类的序列化数据转化成Object)
在readOrdinaryObject中,首先用readClassDesc生成了一个desc,该函数返回类描述信息
desc中主要包含了类名,serialVersionUID(后面会讨论)以及Class信息
紧接着调用desc.newInstance
函数创建一个空的类实例
接下来就是最重要的填充数据的部分了,所有的漏洞都是从这里产生的,核心触发函数是
readSerialData
函数,判断类是否重载了readObject函数,如果重载了则反射调用重载的ReadObject;如果没有重载,则按照默认的defaultReadFields来填充数据(通过遍历所有属性,再次调用)
Java当中默认填充数据的方式,使用的是unsafe的putObject方法,这个方法允许我们直接操作 给定对象的地址值 • 第一个参数是需要修改的对象,第二个参数是偏移,第三个参数是需要填入的值 • 假设传参为(object, 10, 1),则代表修改object对象10偏移处的值为1
演示
// 没有实现Serializable接口
public class Company{
private String companyName;
public void setCompanyName(String name) {
this.companyName = name;
}
public String getCompanyName() {
return companyName;
}
}
public class Person extends Company implements Serializable{
private static final long serialVersionUID = 2L;
private int age;
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public int external;
private void writeObject(ObjectOutputStream s) throws IOException {
//System.out.println("Person WriteObject!");
s.defaultWriteObject();
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 生成了一个Person类实例
Person p = new Person();
p.setName("d_infinite");
p.setAge(10);
p.setCompanyName("alibaba");
//生成一个ObjectOutputStream实例,调用writeObject方法
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.ser"));
objectOutputStream.writeObject(p);
objectOutputStream.close();
//生成一个OIS,调用readObject
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser.ser"));
Object p2 = objectInputStream.readObject();
if(p2 instanceof Person){
Person p3 = (Person)p2;
System.out.println("Unserializable Person Name: " + p3.getName());
System.out.println("Unserializable Person Age: " + p3.getAge());
System.out.println("Unserializable Company Name: " + p3.getCompanyName());
}
}
(companyName没有参与反序列化,值为null)
serialVersionUID
serialVersionUID相对来说是一种协议
当serialVersionUID不一致时,反序列化会直接抛出异常,让我们看下面这个例子,我们设置serialVersionUID为1L时序列化,再修改为2L反序列化,抛出如下异常
Exception in thread "main"java.io.InvalidClassException:Serialize.Person;local class incompatible; stream classdesc serialversionUID = 1,local class serialVersionUID = 2
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamclass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)
at java.io.ObjectInputStream.readOrdinaryobject(objectInputStream.java:2158)
at java.io.ObjectInputStream.readobject(ObjectInputStream.java:1665)
at java.io.ObjectInputStream.readobject(objectInputStream.java:501)
at java.io.ObjectInputStream.readobject(ObjectInputStream.java:459)
at Serialize.Person.main(Person.java:42)
的属性虽然是static
,正常来说是不会参与序列化与反序列化,但是它是比较特殊的
总结
基础差不多就这么多了