前言
在Java安全中反序列化是一个非常重要点,有原生态的反序列化,还有一些特定漏洞情况下的。今天主要讲一下原生态的反序列化,这部分内容对于没Java基础的来说可能有点难,包括我。
序列化与反序列化
序列化:将内存中的对象压缩成字节流
反序列化:将字节流转化成内存中的对象
简单来说就是把对象转换为字节流,方便存储在文件、数据库等。
Java里面常见的序列化与反序列化协议。
JAVA内置的writeObject()/readObject()
JAVA内置的XMLDecoder()/XMLEncoder
XStream
SnakeYaml
FastJson
Jackson
假如我要传输这一串代码,直接复制的话肯定不行,因为里面有大量的空格和换行,直接复制会出错的。所以就要用到序列化,把它加密成一串字节流进行传输,然后再反序列化解密还原成代码即可。
原生反序列化
Java新建一个项目,叫serialization。
随便选个版本即可。
创建一个user类,并写入代码。
再创建一个类叫serialization_demo,用来实现序列化操作的。简单来说代码就是把我们的user类里面的对象进行序列化操作,并且把结果写入ser.txt里面。
package com.sf.maven.serialization;public class serialization_demo {public static void main(String[] args) throws IOException {// 创建一个用户对象,引用UserDemouser u = new user("wlw", "gay", 30);// 调用方法进行序列化SerializableTest(u);// ser.txt 就是对象u序列化的字节流数据}public static void SerializableTest(Object obj) throws IOException {// 使用 ObjectOutputStream 将对象 obj 序列化后输出到文件 ser.txtObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));//序列化操作oos.writeObject(obj);// 关闭流oos.close();}
}
直接运行可以看到输入了我们的对象。
在旁边的目录下可以看到一个ser.txt文件,打开发现里面是字节流,这个ser.txt就是user对象序列化的结果。
我们再来试试反序列化,看看能不能把字节流还原。先创建一个类叫Unserialization_demo,写入以下代码,就是从ser.txt读取字节流,再反序列还原输出。
package com.sf.maven.serialization;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;public class Unserialization_demo {public static void main(String[] args) throws IOException, ClassNotFoundException {// 调用下面的方法,传输 ser.txt,解析还原反序列化Object obj = UnserializableTest("ser.txt");// 对 obj 对象进行输出,默认调用原始对象的 toString 方法System.out.println(obj);}// 反序列化方法public static Object UnserializableTest(String Filename) throws IOException, ClassNotFoundException {// 读取 Filename 文件进行反序列化还原ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));Object o = ois.readObject();// 返回反序列化后的对象return o;}}
直接运行好吧,可以看到输出了对象数据。
这就是原生的序列化和反序列化操作,就是把数据转换成字节流然后再封装成一个文件,然后再还原。
重写方法
OK,那么现在我们来讲一下怎么利用这个玩意,我们可以看到readObject()这个方法是来自ObjectinputStream.java这个文件。
而这个Java文件是来自src.zip里面,是我们JDK自带的东西。
现在我们来重写这个readObject这个方法,什么意思呢,就是当我们进行反序列化的时候不是会调用JDK自带的readObject()吗,那现在我们自己写一个readObject方法,让它在进行反序列的时候去调用我们自己写的readObject,而不是调用JDK自带的readObject。
在我们的user类里面加入重写readObject的代码,就是命令执行计算机。
重新对user进行序列化,生成新的ser.txt文件。
再对ser.txt进行反序列化,成功弹出了计算机!!!说明我们重写的方法有用,在反序列化的时候它执行的是user里面我们自己写的readObject,而不是JDK自带的。
我们来步入跟进一下代码,为了直观我们在readObject添加一行代码。
设置一个断点进行调试。
点击步入可以看到此时的Filename等于我们的ser.txt。
继续步入,执行到我们的ObjectInputStream读取文件这里。
接着步入跳到了ObjectInputStream.java类这里。
此时我们步出回到了这里。
接着步入,让它调用readObject。
关键的来了,此时我们再步入的话,跳转到了我们的user.java里面的readObject,而不是ObjectinputStream里面的readObject。
toString
上面我们说的方法是属于入口类的readObject危险方法直接调用(我也没搞懂为啥叫这个名字),还有方法就是—构造函数/静态代码块等类加载时隐式执行,说实话好绕口啊。其实我感觉两个都差不多,我们先在user里面重写一下toString。
把我们上面重写的readObject注释掉,重新序列化生成一个ser.txt。
再执行反序列化代码,对ser.txt还原,此时也弹出了计算机。
这是为啥,原来当我们用System.out.println()进行输出的时候,会默认调用toString方法,而toString我们上面添加了启动计算机的命令代码。
HashMap
还有就是如果我们使用的类里面自带readObject方法会不会触发呢,HashMap这个类里面就自带readObject,我们用它试一下。先创建一个类叫HashMap_demo,写入以下代码,就是对hash这个对象进行序列化并输出到url.txt,然后再反序列化。
package com.sf.maven.serialization;
import java.io.*;
import java.net.URL;
import java.util.HashMap;public class HashMap_demo implements Serializable {public static void main(String[] args) throws IOException, ClassNotFoundException{HashMap<URL, Integer> hash = new HashMap<>();URL u = new URL("dnslog地址");hash.put(u, 1);//序列化SerializableTest(hash);//反序列化//UnserializableTest("url.txt");}public static void SerializableTest(Object obj) throws IOException {// 使用 ObjectOutputStream 将对象 obj 序列化后输出到文件 url.txtObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("url.txt"));//序列化操作oos.writeObject(obj);oos.close();}public static Object UnserializableTest(String Filename) throws IOException, ClassNotFoundException {// 读取 Filename 文件进行反序列化还原ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));Object o = ois.readObject();return o;}
}
直接运行代码先序列化生成一个url.txt。
接着对url.txt进行反序列,可以看到dnslog有回显,说明代码对dnslog进行了解析。
这是咋回事呢,直接来看一下这个利用链。我们引用了HashMap —> readObject本来在ObjectInputStream里面 —> 但是HashMap里面也有readObject —> 所以在反序列化的时候调用的是HashMap的readObject,而不是ObjectInputStream的readObject—>触发HashMap.putVal() —> 触发HashMap.hash() —> 最终触发URL.hashCode()去访问我们dnslog地址。
如果把dnslog地址换成命令,不就是RCE漏洞了吗,这种就是入口参数包含可控类,改类有危险方法,readObject时调用。
总结
虽然今天的内容看起来有点复杂,但总结起来就一件事,想办法调用其它的readObject,而不是去调用原本的readObject。
最后还是要声明一下,以上仅为个人的拙见,如何有不对的地方,欢迎各位师傅指正与补充,有兴趣的师傅可以一起交流学习。