Java反序列化漏洞基础


一、基本概念

1.0 基本环境

JDK:包含Java运行时环境JRE。

IDEA:开发工具。

Maven : jar包依赖管理。

Tomcat:HTTP服务器。

Burp Suite :发送HTTP请求。

Kali:启动相关服务。

1.1 Java序列化和反序列化基础概念

1.1.1 基本概念

序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。

Java序列化是指把Java对象转换为字节序列的过程(便于保存在内存、文件、数据库中),ObjectOutputStream类的writeObject()方法可以实现序列化,这时jdk原生的序列化方式。Java反序列化是指把字节序列恢复为Java对象的过程,ObjectInputStream类的readObject()方法用于反序列化,这是jdk原生的反序列化方式。

总之:

  • Java 序列化就是把一个 Java Object 变成一个二进制字节数组 , 即 byte[] .

  • Java 反序列化就是把一个二进制字节数组(byte[]) 变回 Java 对象 , 即 Java Object .

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

  1. 持久化内存数据
  2. 网络传输对象
  3. 远程方法调用(RMI)

1.1.3 序列化后的数据特征

早期数据传输的格式:约定定长字段的字符串->指定长度的变长字符串->XML->JSON。

java序列化后的数据可以是可读的字符串,也可以是不可读的二进制数据。

  • 下面是PHP对象反序列化后的特征,是一个可读的明文字符串:
  • FastJson反序列化实现:

一个java对象用不同的协议/库序列化之后,它的表现形式不尽相同,可能是可读的字符串比如json,也可能是不可读的二进制字符串。

  • Java原生反序列化
java.io.ObjectOutputStream.writeObject() //序列化
java.io.ObjectInputStream.readObject() //反序列化
import java.io.Serializable;

public class Person implements Serializable {
    public String name;
    public int age;
    
    public Person(String s, int i) {
        this.name = s;
        this.age = i;
    }
    
    public String getName() { 
        return name; 
    }
    
    public void setName(String name) { 
        this.name = name; 
    }
    
    public int getAge() { 
        return age; 
    }
  
  	public void setAge(String age) { 
        this.age = age; 
    }
}
import java.io.*;

public class Main {
    public static void main(String[] args) 
        throws IOException, ClassNotFoundException { 
        
        // 1.创建Person对象
        Person obj = new Person("wuya", 666); 
        
        // 2.定义文件路径
        String filePath = "D:/wuya.xxxx";    
        
        // 3.序列化操作
        try (ObjectOutputStream outStream = 
            new ObjectOutputStream(new FileOutputStream(filePath))) { // 修正流初始化
            outStream.writeObject(obj);
        }
        
        // 4.反序列化操作
        try (ObjectInputStream inStream = 
            new ObjectInputStream(new FileInputStream(filePath))) { // 修正流初始化
            Person readObject = (Person) inStream.readObject();
            System.out.println("反序列化后: name=" + readObject.name 
                + ", age=" + readObject.age);
        }
    }
}

class Person implements Serializable {
    String name;
    int age;
    
    public Person(String name, int age) { // 规范构造方法
        this.name = name;
        this.age = age;
    }
}

二进制具体的格式讲解:https://blog.csdn.net/lqzkcx3/article/details/79463450

这里是在同一处位置进行序列化和反序列化的,Person类可以自动识别,但实际在网路传输过程中,收发两端需要都具备识别相应对象的条件。

1.2 序列化/反序列化的前提

一个类如果要能实现序列化操作,必须实现Serializable接口或者Externalizable接口。实现java.io.Serializable接口才可被序列化/反序列化,而且所有属性必须是可序列化的(用transient关键字修饰的属性除外,不参与序列化过程)

在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以通过ObjectInputStream与ObejctOutputStream序列化,这是java原生支持的序列化/反序列化方式。如果想知道一个 Java 标准类是否是可序列化的,可以通过查看该类的文档,查看该类有没有实现 java.io.Serializable接口。

所以综上,一个类的对象要想序列化成功,必须满足两个条件:

  • 1.该类必须实现 java.io.Serializable 接口。
  • 2.该类的所有属性必须是可序列化的(如果有一个属性不是可序列化的,则该属性必须注明是短暂的)。

可以被序列化的类,最好还要设置一个**serialVersionUID**属性:

serialVersionUID

每个可序列化的类在序列化时都会关联一个版本号 , 这个版本号就是 serialVersionUID 属性。

在反序列化过程中会根据这个版本号来判断序列化对象的发送者和接收着是否有与该序列化/反序列化过程兼容的类 。如果不相同 , 则会抛出 InvalidClassException 异常。

serialVersionUID 属性必须通过 static final long 修饰符来修饰。

如果可序列化的类未声明 serialVersionUID 属性 , 则 Java 序列化时会根据类的各种信息来计算默认的 serialVersionUID 值。但是 Oracle 官方文档强烈建议所有可序列化的类都显示声明 serialVersionUID 值。

二、漏洞原理分析

2.1 漏洞成因简介

当程序可以反序列化不可信的数据时,攻击者可构造恶意序列化对象,在反序列化过程中触发非预期的代码执行。Java反序列化机制本身不验证数据安全性,依赖开发者自行控制输入来源。但是,如果服务端不存在能够触发命令执行的利用链,攻击者再怎么构造反序列化数据也无济于事。

所以核心问题在于:

  • 1)目标存在不安全的反序列化入口。
  • 2)存在可利用的Gadget链。

Gadget 是什么?

想象你玩乐高积木,想拼出一个能“搞事情”的机关。Gadget(小工具) 就是一组被黑客精心挑选的“积木块”(Java 类和方法),当它们按特定顺序拼接起来时,就能在反序列化过程中触发恶意操作(比如执行命令、下载病毒)。

假设攻击者想通过反序列化让服务器执行 rm -rf /(删光所有文件),他需要找到以下“积木”:

  1. 起点积木:某个类的 readObject() 方法(反序列化时自动调用)。
  2. 中间积木:能传递恶意参数的其他类(比如修改某个配置参数)。
  3. 终点积木:最终触发命令执行的类(比如调用 Runtime.exec("rm -rf /"))。

这些积木连起来就是 Gadget 链

  • 关键特点

    依赖现有代码:Gadget 链中的类必须是目标应用中已有的(比如依赖的第三方库)。

    利用反射/特性:通过反射调用方法、修改私有字段等“非正常操作”拼接逻辑。

    环境敏感:不同 Java 版本、不同依赖库,可用的 Gadget 链可能不同。

  • 防御为何难?

    • Gadget 链千变万化:就像乐高有无数种拼法,开发者很难预测所有可能的恶意组合。

    • 黑名单防不住:总有新的“积木”被挖掘出来(比如某个冷门库的类突然被利用)。

比如我们可以试想一下有如下的Person类,客户端传递一个Person类->给服务端服务端需要反序列化我们传送的数据,那么就需要调用readObject。 而开发者如果在类中重写 readObject,也就给予攻击者在服务器上运行代码的能力,(当然这只是个例子,现实中不会有这么明显的缺陷)。

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
 
public class Person implements Serializable {
    private int age;
    public String name;
    public static int id;
 
    public Person(){
    }
 
    public Person(int age, String name){
        this.age = age;
        this.name = name;
    }
 
    @Override
    public String toString() {
        return "Person{"+
                "name='" + name +'\'' +
                ",age=" + age +
                '}';
    }
 
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        Runtime.getRuntime().exec("calc");
   }
 
}

可以发现,我们的readObject首先 ois.defaultReadObject(); 也就是让反序列化读取的时候按照默认的方法执行。但是我们添加了一行Runtime.getRuntime().exec(“calc”); 也就是windows 操作系统打开计算器的命令,服务器反序列化Person对象的时候会调用Person类的readObject方法(这很好理解,反序列化什么类的对象就用这个类的readObject方法呗),就会打开它的计算器,这也就是RCE(远程代码执行漏洞)的最近本的形式。如果ois是攻击者可以控制的输入端,并且服务端存在类似 Runtime.getRuntime().exec("calc");的恶意代码执行的可能性,那么就会导致RCE高危漏洞。

为什么java会允许我们自己写readObject,让服务端调用我们的readObject呢?这样不是很危险吗?答:有这样的需求。

比如:我们的类中定义了一个 arr 数组属性,初始化的数组长度为100,但是我在数组中可能只存放 30 个数而已。我总不能把100个数全序列化,后面的70个数就变成null了。根本没有意义嘛。于是我们就可以先transient这个数组,然后我们自己写readObject,来实现我们的需求。

private transient Object[] arr;

......

 private void readObject(java.io.ObjectInputStream s)
          throws java.io.IOException, ClassNotFoundException {
 
        s.defaultReadObject();
        arr = new Object[30];


        for (int i = 0; i < 30; i++) {
            arr[i] = s.readObject();
        }
    }

这里我们先执行defaultReadObject(); 这时只有transient字段的arr不会序列化,然后我们自己再专门对arr[]进行readObject处理。这里defaultReadObject不会处理transient字段,但是我们自己专门写的readObject就不管是什么字段都会处理。

我们到这里可以发现,这种漏洞产生的核心是存在危险的代码在服务端触发执行的可能性,并且攻击者能否控制输入来触发危险代码的执行。在漏洞不那么明显的时候,也就是仅满足两个核心条件中的一个时,需要通过构造复杂的利用链来达到执行恶意命令的目的。

2.2 HashMap的登场

直观来讲要想利用反序列化漏洞,接收的类要满足:1、支持序列化/反序列化, 2、重写了readObject方法 ,3、在重写的readObject中存在执行恶意命令的可能,4、可以接收很多类型的参数。(最后一个条件没有前面的那么必要。)

那么java中有哪些常见的类是满足上面的条件的呢?当然最受瞩目的就是HashMap,天生适合。

首先:

1)实现了Serializable接口,支持序列化/反序列化:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
  //......
  //具备serialVersionUID属性 
	private static final long serialVersionUID = 362498820763181265L;
  //......
}

2)重写了readObject方法

private void readObject(java.io.ObjectInputStream s)

3)readObject方法中存在执行恶意命令的可能:(以下基于源码来分析)

  • 1407和1409行:会调用键和值所对应的类的readObject()方法,这些类本身在反序列化的过程中存在能执行恶意代码的可能性。
  • 1410行:putVal函数中会调用hash()函数计算键对象的哈希值(主要是用来确定元素在数组中的存放的位置)。

hash()函数会间接调用键对象的hashCode()方法,本身的目的是返回一个数值用来确定这个键值对应该放在哪里,如果当两个键对象计算出来的hashcode值一样会挂在对应位置的链表或红黑树上,来决绝哈希冲突的问题。但是有些类会重写hashCode方法,并且存在执行恶意代码的可能性,进而可能会导致RCE漏洞。

  • 555行:get(Object key)函数中也存在hash(key)的执行。

并且与putVal方法不同的是,当一些键对象哈希值相同,也就是这些键值对象放在同一个位置上的链表/红黑树上是,getNode函数中还会调用equals方法来逐个比较名字,来找到具体要获取的对象。如果某些类重写了equals方法并包含了恶意代码执行的可能,那么也会导致RCE漏洞。

4)可以接收很多类型的参数:

从HashMap的泛型参数就可以知道HashMap可以容纳各种类型的对象,那么就给了那些可能执行恶意代码的类、或具备恶意gadget链的类保存在其中的可能,再结合条件3,就能够产生RCE漏洞。

2.3 HashMap重写readobject 和 writeobject

基于上述的分析在啰嗦一下,HashMap为什么要重写readobject 和 writeobject呢?

HashMap 需要自定义 readObjectwriteObject 方法,主要是为了 优化序列化性能保证反序列化后的状态一致性。以下是具体原因和实现逻辑:

2.3.3 默认序列化的缺陷

HashMap中由于entry的存放位置由Key的hash值来计算 然后存放在数组中。同一个key在不同的jvm中计算的hash值可能不同 不同的hash值就会导致一个hashmap对象的反序列化结果和序列化之前的结果不一致

比如:可能key=AAA在0的位置,而反序列化之后根据Key获取元素的时候需要从数组2的位置取。 hashmap想要保证一个键值对的唯一性就需要计算hashcode 。如果建的是一个对象,在不同的jvm可能就不一样。所以需要将对象拆开将不同的值单独计算。

以下是HashMap.readObject中的部分代码:

for (int i = 0; i < mappings; i++) {
     @SuppressWarnings("unchecked")
         K key = (K) s.readObject();
     @SuppressWarnings("unchecked")
         V value = (V) s.readObject();
         putVal(hash(key), key, value, false, false);
 }

发现这里使用了putVal 传入了 hash(key)参数

hash函数

static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里调用了key的hashCode方法。

2.4 触发场景

  • 1.HTTP请求中的参数。
  • 2.RMI,即Java远程方法调用,在RMI中传输的数据皆为序列化。
  • 3.JMX,一个为应用程序植入管理功能的框架。
  • 4.自定义协议,用来接收与发送原始的java对象。

2.5 JAVA序列化数据流特征

2.3.1 概述

Java序列化数据流是通过ObjectOutputStream生成的二进制流,其结构具有独特的特征。以下是其核心特征及对应的技术细节:

  • 固定头部标识

魔数(Magic Number)与版本号:所有Java序列化流的起始字节为AC ED(十六进制),紧接着是版本号00 05(表示JDK 1.5及更高版本)。这是识别Java序列化数据的首要标志。

AC ED 00 05  // 固定头部
  • 类元数据描述

类全限定名与字段信息:序列化流中会包含完整的类名、字段名称、类型及顺序。例如,序列化一个Person类时,流中会记录类名com.example.Person,以及字段如name(String类型)、age(int类型)等。

SerialVersionUID:每个可序列化类需定义serialVersionUID(显式或隐式生成)。若未显式定义,JVM会根据类结构自动生成,类结构变化会导致UID变化,可能引发反序列化失败。

  • 数据存储结构

对象与字段的层次化编码:数据流通过特定标记(如TC_OBJECTTC_CLASSDESC)标识对象和类描述符。例如:

TC_OBJECT -> TC_CLASSDESC -> 类名 -> serialVersionUID -> 字段列表 -> 字段值[3,12](@ref)

类型标识符:基本类型(如intI标记)、对象类型(如StringLjava/lang/String;)均有固定编码。

  • 涉及到以下函数,则考虑JAVA反序列化:
ObjectInputStream.readobject
ObjectInputStream.readUnshared
XMLDecoder.readObject
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject
.....

2.3.2 序列化

通过查看序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号0x00 05的数据。此外还包含了类名、成员变量的类型和个数等。

对象所属类:

public class SerialObject implements Serializable{
    private static final long serialVersionUID = 5754104541168322017L;

    private int id;
    public String name;

    public SerialObject(int id,String name){
        this.id=id;
        this.name=name;
    }
    ...
}

序列化SerialObject实例后以二进制格式查看:

00000000: aced 0005 7372 0024 636f 6d2e 7878 7878  ....sr.$com.xxxx
00000010: 7878 2e73 6563 2e77 6562 2e68 6f6d 652e  xx.sec.web.home.
00000020: 5365 7269 616c 4f62 6a65 6374 4fda af97  SerialObjectO...
00000030: f8cc c5e1 0200 0249 0002 6964 4c00 046e  .......I..idL..n
00000040: 616d 6574 0012 4c6a 6176 612f 6c61 6e67  amet..Ljava/lang
00000050: 2f53 7472 696e 673b 7870 0000 07e1 7400  /String;xp....t.
00000060: 0563 7279 696e 0a                        .cryin.

序列化的数据流以魔术数字和版本号开头,这个值是在调用ObjectOutputStream序列化时,由writeStreamHeader方法写入:

protected void writeStreamHeader() throws IOException {
     bout.writeShort(STREAM_MAGIC);//STREAM_MAGIC (2 bytes) 0xACED
     bout.writeShort(STREAM_VERSION);//STREAM_VERSION (2 bytes) 5
}

2.3.3 反序列化

Java程序中类ObjectInputStreamreadObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID

如果类描述符是动态代理类,则调用resolveProxyClass方法来获取本地类。如果不是动态代理类则调用resolveClass方法来获取本地类。如果无法解析该类,则抛出ClassNotFoundException异常。

如果反序列化对象不是String、array、enum类型,ObjectStreamClass包含的类会在本地被检索,如果这个本地类没有实现java.io.Serializable或者externalizable接口,则抛出InvalidClassException异常。因为只有实现了SerializableExternalizable接口的类的对象才能被序列化。

readObject()方法在反序列化漏洞中它起到了关键作用,readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法。如果该方法书写不当的话就有可能引发恶意代码的执行。

三、检测Java反序列化漏洞

3.1 代码审计

重点关注一些反序列化操作函数并判断输入是否可控。

基本思路:

1、通过检索源码中对反序列化函数的调用来静态寻找反序列化的输入点

例如,可以搜索以下函数:

ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject

类名.方法名

2、确定了反序列化输入点后,再考察应用的Class Path中是否包含Apache Commons Collections等危险库(ysoserial所支持的其他库亦可) https://github.com/frohoff/ysoserial/

为*Class Path中的危险库

Class Path是Java应用加载类文件的路径,包括项目依赖的第三方库(如JAR文件)。

  • **Apache Commons Collections (≤3.2.1)**:其中的InvokerTransformerTransformedMap等类可通过反射执行任意方法。
  • **Fastjson (旧版本)**:反序列化时自动调用setter方法或特定构造方法。
  • JDK内部类:如java.util.HashMapjavax.management.BadAttributeValueExpException等,可能被用于构造攻击链。

为何需要检查Class Path中的库?

反序列化漏洞的利用需要满足以下条件:

  • 存在入口:程序接收外部输入并调用readObject()
  • 存在攻击链:依赖库中存在多个可串联的类(Gadget Chain),能通过反射、动态代理等机制执行恶意操作。

ysoserial的作用

ysoserial是一个工具,用于生成针对不同库的攻击Payload。其核心原理是:

  • 预定义攻击链:根据目标库(如Commons Collections、Fastjson)的漏洞类,自动构造恶意对象。
  • 生成序列化数据:将攻击链对象序列化为字节流,供攻击者注入到反序列化入口。

支持的库列表

  • Commons Collections (3.x, 4.x)
  • Groovy
  • Hibernate
  • JDK内部类(如Jdk7u21)
  • Fastjson(特定版本)

实际利用过程:

假设目标应用存在反序列化入口,且依赖了Apache Commons Collections 3.2.1:

1)*生成Payload**:使用ysoserial生成命令执行的Payload。

java -jar ysoserial.jar CommonsCollections5 "calc.exe" > payload.bin

2)注入Payload:将payload.bin发送到目标的反序列化入口(如HTTP请求体、RMI调用等)。

3)触发漏洞:目标应用反序列化payload.bin时,执行calc.exe

如何检查应用是否包含危险库?

检查依赖文件:查看项目的pom.xml(Maven)、build.gradle(Gradle)或lib/目录中的JAR文件。

版本确认:通过代码或工具(如mvn dependency:tree)确认依赖库版本。

常见危险库标识,如:

  • Apache Commons Collections 3.x:文件名如commons-collections-3.2.1.jar
  • Fastjson ≤1.2.24:存在autoType漏洞。

3、若不包含危险库,则查看一些涉及命令、代码执行的代码区域,防止程序员代码不严谨,导致产生漏洞

4、若包含危险库,则使用ysoserial进行攻击复现

3.1.1 白盒审计

1)定位反序列化入口点

关键方法搜索:全局搜索代码中所有调用ObjectInputStream.readObject()、XMLDecoder.readObject()、Yaml.load()等反序列化方法的位置。重点关注网络通信、文件读取、缓存加载等场景,例如HTTP请求参数解析、Redis数据反序列化等。

协议特征识别:若数据流以AC ED 00 05(Java原生序列化头部)或rO0AB(Base64编码特征)开头,需优先审查其处理逻辑。

2)检查依赖库的Gadget链风险

  • 危险库版本分析

检查pom.xml或build.gradle中是否包含已知存在漏洞的库(如Apache Commons Collections ≤3.2.1、Fastjson ≤1.2.68、XStream <1.4.19等)。

使用工具(如mvn dependency:tree)生成依赖树,确认是否存在可利用的Gadget链组件(如InvokerTransformer、TemplatesImpl等)。

  • 动态代理与反射调用:

审查代码中是否使用Proxy.newProxyInstance()创建动态代理对象,或通过AnnotationInvocationHandler触发隐式方法调用(如equalsImpl遍历memberValues)。

3)攻击链逻辑审计

  • 危险方法调用路径

跟踪readObject()方法中是否调用了高风险操作(如Runtime.exec()、JNDI.lookup()、反射invoke()等)。

检查是否存在串联调用链,例如:

// CommonsCollections链示例
ChainedTransformer.transform() → InvokerTransformer.invoke() → Runtime.exec()

此类链式调用需结合动态代理触发点(如LazyMapTransformedMap)。

  • 输入参数可控性验证

确认反序列化数据来源是否可控(如用户输入的HTTP参数、文件内容),并分析是否有过滤或编码缺失。

4)防御机制审计

  • 输入验证与过滤:

检查是否使用白名单机制(如Java 9+的ObjectInputFilter)限制可反序列化的类。

验证代码中是否对@type(Fastjson场景)或Serializable接口实现类进行严格校验。

  • 异常处理与日志监控:

确保反序列化失败时不会泄露类路径、堆栈信息等敏感数据。

检查是否记录反序列化操作的异常日志(如ClassNotFoundException),以便追踪攻击行为。

5)工具辅助验证

静态扫描工具:

使用Find Security Bugs、SonarQube检测代码中的反序列化风险点。

小结

Java反序列化漏洞白盒审计需结合入口点定位、依赖库分析、攻击链重构和防御机制验证四步法。通过工具辅助与人工逻辑分析,重点排查readObject()的重写风险及第三方库的Gadget利用可能性。防御层面应优先采用白名单过滤和输入校验,避免信任不可控的序列化数据。

3.2 黑盒审计

调用ysoserial并依次生成各个第三方库的利用payload(也可以先分析依赖第三方包量,调用最多的几个库的paylaod即可),该payload构造为访问特定url链接的payload,根据http访问请求记录判断反序列化漏洞是否利用成功。如:

java -jar ysoserial.jar CommonsCollections1 'curl " + URL + " '

也可通过DNS解析记录确定漏洞是否存在。现成的轮子很多,推荐NickstaDB写的SerialBrute,还有一个针对RMI的测试工具BaRMIe。

3.3 攻击检测

通过查看反序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号。我只见到过版本号为5(0x00 05)的数据。考虑到zip、base64各种编码,在攻击检测时可针对该特征进行匹配请求post中是否包含反序列化数据,判断是否为反序列化漏洞攻击。

xxxdeMacBook-Pro:demo xxx$ xxd objectexp 
    00000000: aced 0005 7372 0032 7375 6e2e 7265 666c  ....sr.2sun.refl
    00000010: 6563 742e 616e 6e6f 7461 7469 6f6e 2e41  ect.annotation.A
    00000020: 6e6e 6f74 6174 696f 6e49 6e76 6f63 6174  nnotationInvocat
    00000030: 696f 6e48 616e 646c 6572 55ca f50f 15cb  ionHandlerU.....

但仅从特征匹配只能确定有攻击尝试请求,还不能确定就存在反序列化漏洞,还要结合请求响应、返回内容等综合判断是否确实存在漏洞。

3.4 RASP检测

Java程序中类ObjectInputStreamreadObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。

类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以RASP检测反序列化漏洞时可通过重写ObjectInputStream对象的resolveClass方法获取反序列化的类即可实现对反序列化类的黑名单校验。

其他

1.从流量中发现序列化的痕迹,关键字:ac ed 00 05,rO0AB。

2.Java RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口。

3.从源码入手,可以被序列化的类一定实现了Serializable接口。

4.观察反序列化时的readObject()方法是否重写,重写中是否有设计不合理,可以被利用之处。

从可控数据的反序列化或间接的反序列化接口入手,再在此基础上尝试构造序列化的对象。

ysoserial是一款非常好用的Java反序列化漏洞检测工具,该工具通过多种机制构造PoC,并灵活的运用了反射机制和动态代理机制,值得学习和研究。

四、防御Java反序列化漏洞

4.1 类白名单校验

ObjectInputStream中resolveClass 里只是进行了class 是否能被load,自定义ObjectInputStream, 重载resolveClass的方法,对className 进行白名单校验:

public final class test extends ObjectInputStream{
    ...
    protected Class<?> resolveClass(ObjectStreamClass desc)
            throws IOException, ClassNotFoundException{
         if(!desc.getName().equals("className")){
            throw new ClassNotFoundException(desc.getName()+" forbidden!");
        }
        returnsuper.resolveClass(desc);
    }
      ...
}

4.2 禁止JVM执行外部命令Runtime.exec

通过扩展SecurityManager可以实现:

SecurityManager originalSecurityManager = System.getSecurityManager();
        if (originalSecurityManager == null) {
            // 创建自己的SecurityManager
            SecurityManager sm = new SecurityManager() {
                private void check(Permission perm) {
                    // 禁止exec
                    if (perm instanceof java.io.FilePermission) {
                        String actions = perm.getActions();
                        if (actions != null && actions.contains("execute")) {
                            throw new SecurityException("execute denied!");
                        }
                    }
                    // 禁止设置新的SecurityManager,保护自己
                    if (perm instanceof java.lang.RuntimePermission) {
                        String name = perm.getName();
                        if (name != null && name.contains("setSecurityManager")) {
                            throw new SecurityException("System.setSecurityManager denied!");
                        }
                    }
                }

                @Override
                public void checkPermission(Permission perm) {
                    check(perm);
                }

                @Override
                public void checkPermission(Permission perm, Object context) {
                    check(perm);
                }
            };

            System.setSecurityManager(sm);
        }

五、Apache Commons Collections反序列化漏洞

目标:

  1. 哪里出现了可以执行任意代码的问题?
  2. 反序列化的payload怎么构造?

5.1 漏洞爆出

  • 2015年黑客Gabriel Lawrence和Chris Frohoff发现。
  • 影响webLogic、WebSphere、JBoss、Jenkins、OpenNMS等大型框架。

5.2 复现环境

  • idk 1.7.0 80 (不一定)
  • IDEA Project Structure、Settings–Java compile等设置成iava7
  • Apache Commons Collections ≤ 3.2.1

5.3 Apache Commons Collections介绍

5.3.1 Java集合(List、Map、Set)-容器类

Apache Commons Collections是对JDK中容器类的升级扩展,新增的容器包括但不限于:

截屏2025-03-01 14.13.36

5.3.2 使用

5.4 Java反射机制

5.4.1 Java代码运行原理

1、源码

2、编译器 Jjavac)编译为字节码.class文件(我们之所以能在IDEA中看到class对应的源代码,是IDEA反编译的结果,成常来说class文件是二进制形式的,如果用010editor打开就是二进制流,开头是CAFE BEBE。)

3、各平台的JVM解释器把字节码文件转换成操作系统指令,这也是跨平台的原因。

5.4.2 反射机制的用途

在程序运行的时候动态创建一个类的实例,调用实例的方法和访问它的属性。

Java反射机制就像程序在运行时的“透视镜”和“万能钥匙”,它允许程序在不知道具体类结构的情况下,动态地“看透”类的内部构造并进行操作。例如,当你在代码中写new Person()时,类的结构是编译时确定的;而反射则能让程序在运行时通过类名(如从配置文件读取的字符串)加载一个未知的类,创建对象、调用方法,甚至修改私有字段的值。这种能力让框架(如Spring)能自动管理对象依赖、动态注入属性,也让工具(如JSON解析器)能通过字段名自动序列化对象,而不需要提前知道类的具体定义。简单来说,反射让Java代码具备了“运行时自我调整”的灵活性,是框架和动态功能实现的核心技术。

静态:源代码中new对象,动态:程序动态创建Class。

public static void test1() throws IOException {
    Runtime.getRuntime().exec("calc");
}

public static void test2() {
    try {
        // 1. 获取 Runtime 类
        Class<?> clazz = Class.forName("java.lang.Runtime");
        
        // 2. 获取 getRuntime() 方法并调用(静态方法,参数为 null)
        Object runtime = clazz.getMethod("getRuntime").invoke(null);
        
        // 3. 获取 exec() 方法并执行命令(需传入 Runtime 实例和命令参数)
        clazz.getMethod("exec", String.class).invoke(runtime, "calc");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

5.5 Apache Commons Collections漏洞原理

5.5.1 关键的类

  • InvokeTransformer:利用Java反射机制来创建类实例。

  • ChainedTransformer:实现了Transformer链式调用,我们只需要传入一个Transformer数组,ChainedTransformer就可以实现依次的去调用每一个Transformer的transform()方法。

  • ConstantTransformer:transform0返回构造函数的对象

  • TransformedMap

5.5.2 调用链路概览

5.5.3 POC构造思路

1、InvokeTransformer 反射执行代码。

2、ChainedTransformer 链式调用,自动触发。

3、ConstantTransformer 获得对象。

4、TransformedMap 元素变化执行transform, setvalue–checkSetValue。

5、AnnotationInvocationHandler readObject 调用Map的setValue。

调试分析:(ctrl+f12 查看所有的属性和方法)

漏洞位置:InvokeTransformer 反射执行代码:

漏洞触发:

截屏2025-03-01 14.59.43

ChainedTransformer 链式调用,自动触发

最终的POC:

如何让目标反序列化执行上面的代码呢:

POC调用过程:

截屏2025-03-01 15.30.19

六、Alibaba Fastison反序列化漏洞

复现环境:<=1.2.24 <=1.2.47 (vulhub)

6.1 Fastjson介绍

https://github.com/orgs/alibaba/repositories

Druid、Fastjson、Dubbo、OceanBase、Tengine、TFS、RocketMQ、Canal ……

https://github.com/alibaba/fastjson/wiki/Quick-Start-CN 【文档】

6.2 漏洞复现

<=1.2.24 RCE CNVD-2017-02833 <=1.2.47 RCE CNVD-2019-22238

6.3 漏洞原理

6.4 漏洞挖掘思路

6.5 漏洞修复

七、Apache Shiro反序列化漏洞

参考


文章作者: 司晓凯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 司晓凯 !
  目录