浅谈基于Java的反序列化漏洞

Java的序列化:

与php的序列化反序列化一样,Java的序列化从理解上来看大同小异,都将对象以一连串的字节保存在磁盘文件中的过程,而反序列化也一样是将保存在磁盘文件中的java字节码重新转换成java对象的过程。(默认大家对php序列化有基本概念,以及对“类”有一定的知识基础,如果没有,可以看看我的基于js的原型链污染)

如何实现:

使用JDK类库提供的序列化API:“java.io.ObjectOutputStream”和“java.io.ObjectInputStream”

java.io.ObjectOutputStream表示对象输出流,其中writeObject(Object obj)方法可以将给定参数的obj对象进行序列化,将转换的一连串的字节序列写到指定的目标输出流中。

java.io.ObjectInputStream该类表示对象输入流,该类下的readObject(Object obj)方法会从源输入流中读取字节序列,并将它反序列化为一个java对象并返回。

有点像php的魔术方法,但又不是一个东西。

这两个类负责对象的输入输出,但要实现序列化还需要一个Serializable接口,该接口用于标记某个类可以被序列化。如果不使用标记接口,序列化过程会报错。

Java类基础:

一下代码实现了一个Java的类应用。

// Address.java
import java.io.Serializable;
​
public class Address implements Serializable {
    private String street;
    private String city;
​
    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }
​
    public String getStreet() {
        return street;
    }
​
    public String getCity() {
        return city;
    }
}
// Person.java
import java.io.Serializable;
​
public class Person implements Serializable {
    private String name;
    private Address address;
​
    public Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
​
    public String getName() {
        return name;
    }
​
    public Address getAddress() {
        return address;
    }
}
//Main.java
import com.example.Person;
import com.example.Address;
​
public class Main {
    public static void main(String[] args) {
        Address address = new Address("Main Street", "City");
        Person person = new Person("John Doe", address);
​
        System.out.println("Name: " + person.getName());
        System.out.println("Address: " + person.getAddress().getStreet() + ", " + person.getAddress().getCity());
    }
}

首先我们定义了一个公共类:Address类,并单独放入一个Java文件中(在Java中,每个公共的自定义类都要单独写入一个Java文件,如果在一个文件中写入了多个公共类,那么大概率会导致代码执行出错)。import java.io.Serializable用于后续序列化(当前没用),public class Address implements Serializable 定义了Address类并实现了Serializable接口(为序列化铺垫),在类中定义了两个私有字符串属性street和city, public Address(String street, String city)对Address类的实现, public String getStreet() {return street;}定义方法getStreet()获取对象的属性值,getCity()同理。

定义的Person类同理,不同的是Person类的属性中引用了Address类。(在接下来的序列化中,当一个类A引用了其他类B的对象时,若A的对象被序列化,则B在A内的对象也会被序列化,所以说序列化是递归进行的)

Main:接收字符串作为参数,创建对象address和person并输出。结果如下:

Name: John Doe
Address: Main Street, City

以上就是对Java的类进行一个简单的应用。

如何序列化:

import java.io.*;
//import java.io.FileOutputStream;
//import java.io.ObjectOutputStream;
//import java.io.IOException;
//import java.io.Serializable;
​
​
public class SerializationExample {
    public static void main(String[] args) {
        Address address = new Address("123 Main St", "New York");
        Person person = new Person("Alice", address);
​
        try {
            FileOutputStream fileOut = new FileOutputStream("person.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(person);
            out.close();
            fileOut.close();
            System.out.println("Serialized data is saved in person.ser");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

因为需要用到//import java.io.FileOutputStream;//import java.io.ObjectOutputStream;//import java.io.IOException;//import java.io.Serializable;所以直接引用所有的io类/接口即import java.io.*;之后定义了一个公共类SerializationExample,创建新的Address和Person对象并初始化传参(为了能调用没有被实例化的main函数,需要定义为静态方法)。接下来的语句涉及到FileOutputStream和ObjectOutputStream类,他们可能抛出I/O异常的风险,所以要使用try/catch进行异常情况的处理。main中创建了一个person.ser文件,并通过writeObject把序列化的结果输出在该文件中。结果:

打开查看:

发现是乱码,很正常,在 Java 中,对象序列化后的结果是一种二进制数据,使用文本文档打开会看到一些乱码内容。

接下来是反序列化:

首先要对Person类和Address类的代码进行改造:

// Address.java
import java.io.Serializable;
​
public class Address implements Serializable {
    private String street;
    private String city;
​
    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }
​
    public String getStreet() {
        return street;
    }
​
    public String getCity() {
        return city;
    }
//以下新加入的代码
    @Override
    public String toString() {
        return "Address{" +
                "street='" + street + '\'' +
                ", city='" + city + '\'' +
                '}';
    }
}
// Person.java
import java.io.Serializable;
​
public class Person implements Serializable {
    private String name;
    private Address address;
​
    public Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
​
    public String getName() {
        return name;
    }
​
    public Address getAddress() {
        return address;
    }
//以下是新加入的代码
    @Override
    public String toString() {
        return "Person{name='" + name + "', address='" + address + "'}";
    }
}

@Override是重写标识,这里需要对toString()方法进行重写,写成固定格式的字符串进行输出,如果不进行重写,在调用toString方法时会默认调用 Object 类中的 toString() 方法,并返回包含类名和哈希码的默认字符串表示。接下来写入反序列化代码:

import java.io.*;
​
public class DeserializationExample {
    public static void main(String[] args) {
        try {
            FileInputStream fileIn = new FileInputStream("person.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            Person person = (Person) in.readObject();
            in.close();
            fileIn.close();
            System.out.println("Deserialized data:");
            System.out.println(person);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

改代码调用了person.ser文件,将其序列化的 结果进行反序列化然后输出到控制台:

如果在定义类的代码中不对toString类进行重写,那么输出应该是这样的:

默认输出了类名和后面的哈希码。

其他注意:

已经初步了解了Java的反序列化的原理,接下来介绍一些在Java序列化中的一些注意事项:

transient:在 Java 中,transient 是一个修饰符,用于标记类的成员变量,表示不希望被序列化。当一个对象被序列化时,会将其状态保存为字节流,transient 可以用于排除某些成员变量,使其在序列化过程中被忽略。比如我们基于Address类,把city改为transient

    private transient String city;

再次进行序列化,然后反序列化:

city没有被序列化,因此Address类的city属性是空的。

Externalizable接口:除了使用Serializable接口外,还可以使用Externalizable接口,Externalizable接口相比于Serializable接口更加灵活。Serializable接口使用默认序列化方式,可以序列化所有非transient(即非瞬态)字段,无需编写任何额外的代码。而若使用Externalizable接口使用自定义序列化方式,需要手动控制序列化和反序列化过程。就好比要保存一本书,Serializable接口是直接将整本书存入书架,而Externalizable接口是可以手动地将其中一些需要地部分保存起来放入暑假。使用原来的例子改为Externalizable方式:

//Address
import java.io.*;
​
public class Address implements Externalizable {
    private String street;
    private String city;
​
    public Address() {
    }
​
    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }
​
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(street);
        out.writeObject(city);
    }
​
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        street = (String) in.readObject();
        city = (String) in.readObject();
    }
​
    public String getStreet() {
        return street;
    }
​
    public String getCity() {
        return city;
    }
​
    @Override
    public String toString() {
        return "Address{street='" + street + "', city='" + city + "'}";
    }
}
//Person
import java.io.*;
​
public class Person implements Externalizable {
    private String name;
    private Address address;
​
    public Person() {
    }
​
    public Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
​
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeObject(address);
    }
​
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        address = (Address) in.readObject();
    }
​
    public String getName() {
        return name;
    }
​
    public Address getAddress() {
        return address;
    }
​
    @Override
    public String toString() {
        return "Person{name='" + name + "', address='" + address + "'}";
    }
}

和使用Serializable接口相比,主要有以下不同:

1.增加了额外的无参数构造方法,如

    public Person() {
    }

这是因为使用Externalizable接口时需要用到一个无参数的构造,与Serializable接口相比,Externalizable接口更灵活,可以自己选择需要被序列化的对象,因此在使用前需要一个空白构造。

2.增加了

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeObject(address);
    }

通过传递一个实现了 ObjectOutput 接口的对象(out),writeExternal() 方法可以利用这个对象的方法来完成对象的序列化过程。这个过程可能会引发I/O异常,所以要进行异常处理。方法内可以手动选择被序列化的对象。

3.增加了

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        address = (Address) in.readObject();
    }

通过传递一个实现了 ObjectOutput 接口的对象(out),writeExternal() 方法可以利用这个对象的方法来完成对象的反序列化过程。这个过程可能会引发I/O异常或者类加载可能出现的异常,所以要进行异常处理。方法内可手动选择被反序列化的对象。

序列化类,反序列化类的代码和之前一样,没有区别。

注意:若使用Externalizable接口,可以手动选择对象是否被序列化/反序列化,不需要像Serializable接口一样给不被序列化/反序列化的对象的类型加上transient,就算加上了也没用:

public class Address implements Externalizable {
    private String street;
    private transient String city;

输出:

漏洞产生:

示例:

import java.io.*;
 
class test{
    public static void main(String[] args) throws Exception{
        MyObject myObj = new MyObject();
        FileOutputStream out1 = new FileOutputStream("password.ser");
        ObjectOutputStream out2 = new ObjectOutputStream(out1);
        out2.writeObject(myObj);
        out2.close();
        FileInputStream in1 = new FileInputStream("password.ser");
        ObjectInputStream in2 = new ObjectInputStream(in1);
        in2.readObject();
        in2.close();
    }
}
class MyObject implements Serializable{
    public String flag;
    private void readObject(ObjectInputStream flag) throws IOException, ClassNotFoundException{
        flag.readObject();
        System.out.println("打开了D盘的flag");
        Runtime.getRuntime().exec("explorer.exe D:\\flag.txt");
    }
}

由于 MyObject 类实现了 Serializable 接口并定义了 private void readObject(ObjectInputStream flag) 方法,因此在新对象反序列化过程中,Java 虚拟机会自动调用 readObject(ObjectInputStream flag) 方法,并将反序列化流作为参数传入。因此当MyObject类作为一个恶意类被写入到一个正常的序列化代码中时,就会引发命令执行等危险后果,上述代码的运行结果:

漏洞判断:

用16进制打开序列化结果:

都有共同点,都以AC ED开头,版本号一般时00 05。可以作为判断Java序列化的依据。

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值