JAVA反序列化基础

 

原理

什么是序列化和反序列化

所谓序列化,就是指将内存中的某个对象压缩成字节流的形式,而反序列化,则是将字节流转化成内存中的对象。

反序列化本身是为了还原对象本身,不存在安全问题,但问题在于不可控的在还原对象时候产生了“额外的操作”

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反射获取对应的类信息

(如图) image-20220607220940068

在反序列化对象的时候,跟进readObject,在readObject0中,核心处理类的方法是readOrdinaryObject

image-20220607220949619

(readOrdinaryObject函数负责将非特定类的序列化数据转化成Object)

image-20220607221010730

在readOrdinaryObject中,首先用readClassDesc生成了一个desc,该函数返回类描述信息

desc中主要包含了类名,serialVersionUID(后面会讨论)以及Class信息

image-20220607221202949

紧接着调用desc.newInstance函数创建一个空的类实例

image-20220607221240383

接下来就是最重要的填充数据的部分了,所有的漏洞都是从这里产生的,核心触发函数是 readSerialData函数,判断类是否重载了readObject函数,如果重载了则反射调用重载的ReadObject;如果没有重载,则按照默认的defaultReadFields来填充数据(通过遍历所有属性,再次调用)

image-20220607221254346

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());
        }
}

image-20220607221506167 (companyName没有参与反序列化,值为null)

serialVersionUID

serialVersionUID

相对来说是一种协议

serialVersionUID不一致时,反序列化会直接抛出异常,让我们看下面这个例子,我们设置serialVersionUID为1L时序列化,再修改为2L反序列化,抛出如下异常

image-20220607221520472

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)
serialVersionUID

的属性虽然是static,正常来说是不会参与序列化与反序列化,但是它是比较特殊的

总结

基础差不多就这么多了