Java反序列化1-反射与URLDNS链
1. 反射
通过Demo来学习反射
1.1 创建Person类
1 | public class Person implements Serializable { // implements 实现 Serializable序列化接口 说明支持被反序列化 |
1.2 用Demo理解反射
1.2.1 四种反射
这里会用到四种反射Demo为了各种应用场景,这四种反射都需要有涉猎
Class.forname("包名")使用Class类中的forName()静态方法(最安全,性能最好)类名.class调用类的class属性类获取该类对应的Class对象对象.getClass()调用某个类的对象的getClass()方法对象.getClass().getClassLoader().loadClass("包名")通过类加载器动态加载
1.2.2 Class.forname()
反射无参构造函数
1 | // 1.获取Person的Class对象 |

分析一下这段代码:
- 先通过Class.forname()获取Person的Class对象
- 再获取Class对象中的无参构造器去实例化
newInstance()一个对象,返回值是一个对象 - 查看对象是否是Person类,并确定地址
- 用instanceof判断是否是Person的对象
- 最后判断两个对象是否一样,因为这里的地址是不一样的
@4d7e1886所以我们可以知道这是两个对象
无参构造设置属性和调用方法
1 | Class<?> clazz = Class.forName("com.example.reflection.entity.Person"); |

分析一下这段代码:
先通过无参构造函数获取对象,由于这里区分正反向,我们在正向操作时进行强转才可以调用方法,在反射中我们默认是不知道这个对象的,我们可以通过反射获取Object即可
反射式通过Class对象静态加载后通过getMethod方法
第一个参数是
方法名第二个参数是
参数类型,在源码中是可变参数,无参就不填,有参就填参数类型读一下这个方法的源码:

调用反射获取的方法,
invoke可以理解为是Method对象的调用方法通过反射获取字段属性通过
getField()和getDeclaredField()获取,区别是getDeclaredField可以获取private修饰的属性
反射全参构造函数
1 | Class<?> clazz = Class.forName("com.example.reflection.entity.Person"); |

分析一下这段代码:
- 我们Person对象全参构造器有两个参数,将两个参数的class作为参数传入
getConstructor()方法中再传入两个实参进行实例化 - 查看反射出来的结果是否是Person对象
- 将结果打印查看一下
全参构造设置属性和调用方法
1 | Class<?> clazz = Class.forName("com.example.reflection.entity.Person"); |

分析一下这段代码:
- 我们在获取全参构造函数实例的一个对象之后,先通过他的
getId()方法获取一下id,看看是否获取成功 - 然后调用
canDo()是否正常执行 - 再通过
setId()方法给id设置一个新值,最后打印查看已经完全替换 - 通过field获取id的值,在private修饰的私有属性时,就需要设置Accessible为true,否则不允许读写
- 然后我们看到通过修改,id变为3
1.2.3 类.class
所有方法基本与
Class.forname()相同,只有在获取Class对象的途径上有所区别
1 | // 1.获取Person的Class对象 |
相关内容与Class.forname()相同
1.2.4 对象.getClass()
1 | // 1.获取Person的Class对象 |
相关内容与Class.forname()相同
1.2.5 什么是ClassLoader
我们可以简单理解为Class的加载器(找.class 的文件然后将他解析执行),所有加载的本质都是通过ClassLoader去执行。
1.2.6 getClassLoader()
这里通过getClassLoader()获取Class对象,这里有多种方式,主要从上几种衍生而来。
1 | // 1.方式1:通过类.class找他的ClassLoader去加载Class |
就是对上三种的加载基础上转变一下使用ClassLoader来加载。
2. URLDNS链
2.1 URLDNS链
1 | HashMap<URL, Integer> hashMap = new HashMap<>(); |
2.2 序列化与反序列化
1 | FileOutputStream fileOutputStream = new FileOutputStream("ser.bin"); |
2.5 结果

2.6 Gadget利用链分析
流程
HashMap.readObject() -> HashMap.putVal()->HashMap.hash()->URL.hashcode()->URLStreamHandler().hashCode().getHostAddress->URLStreamHandler().hashCode().getHostAddress().getByName()
1 | 1. HashMap->readObject() |
原理
java.util.HashMap实现了Serializable接口,重写了readObject, 在反序列化时会调用hash函数计算key的hashCode,而java.net.URL的hashCode在计算时会调用getHostAddress来解析域名, 从而发出DNS请求。
分析
- 入口类重写readObject方法
- 入口类可传入任意对象(这种类一般为集合类)
- 执行类可被利用执行危险或任意函数
这条链的入口类是java.util.HashMap,入口类的条件是:
实现Serializable接口
重写readObject()方法
接收参数宽泛
多为JDK自带
HashMap
首先看一下HashMap,这个类实现了Serializable接口

重写了readObject方法,重写方法因为HashMap<K,V>存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,在反序列化过程中就需要对Key进行hash,这样一来就需要重写readObject方法。


putVal()就是哈希表结构存储函数,它调用了hash函数,根据key产生hash。
查看hash()方法,通过相与和位运算计算hash值:

很多类中都具有hashCode方法(用来进行哈希),所以接下来考虑有没有可能存在某个特殊的类M,其hashCode方法中直接或间接可调用危险函数。这条URLDNS链中使用的执行类就是URL类。看URL类之前,还需要确定一下HashMap在readObject过程中能够正常执行到putVal()方法这里,以及传入hash方法中的参数对象keys是可控的。
首先可以看到,参数对象Key由s.readObject()获取,s是输入的序列化流,证明key是可控的。只要mappings的长度大于0,也就是序列化流不为空就满足利用条件。


可以通过HashMap 反序列化调用K/V的readObject() 。
URL
入口类HashMap已经分析完成,具备了利用条件,具体在分析一下URL类的hashCode方法。

可以看到当hashCode属性的值为-1时,跳过if条件,执行handler对象的hashCode方法,并将自身URL类的实例作为参数传入。

进入了URLStreamHandler对象的hashCode()方法,接收URL类的实例,调⽤getHostAddress⽅法。

继续跟进getHostAddress⽅法,getHostAddress方法中会获取传入的URL对象的IP,也就是会进行DNS请求,这⾥ InetAddress.getByName(host) 的作⽤是根据主机名,获取其IP地址,在⽹络上其实就是⼀次DNS查询。
调试流程
先看正向分析

先将断点打在put()上:

强制步进后进入HashMap的put方法:

然后调用HashMap的hash() 方法:

然后进入URL 的hashCode()的方法(传入的参数是Object,就是我们传入的URL):

只有在hashCode这个字段值是-1的情况下,我们才可以进入handler.hashCode() ,到URLStreamHandler 对象的hashCode() 上

进入到getHostAddress()方法:

然后进入getByName()请求发包。
再看反序列化流程分析
现在这两处下断,先通过readObject()将HashMap反序列化,我们这里以Key传URL。


key这个对象已经是URL对象了,我们需要调用的是URL中的hashCode()方法,需要通过hash() 方法执行,接着往里调试。

到了Object.hashCode() 这里,我们知道就是URL.hashCode()

因为我们修改过hashCode=-1,就可以执行我们要发请求的方法handler.hashCode() ,是调用URLStreamHandler对象的hashCode()方法。

然后在getHostAddress() 中发DNS请求。
2.7 阅读ysoserial的POC
1 | public class URLDNS implements ObjectPayload<Object> { |
这里主要阅读getObject()这代码:
new SilentURLStreamHandler()首先实例化一个自定义的继承于URLStreamHandler的类,URLStreamHandler在我们链中的hashCode()这就是调用getHostName()的关键位置,他继承了一个空方法,所以调用的还是原来URLStreamHandler的值。new HashMap()是我们的入口点,后面就如同Gadget利用链一样Relections.setFieldValue()是封装过的class.getDeclaredField()所以全部流程与Gadget一样,后在反序列化中同样执行put()->putVal()->hash()->hashCode()->getHostName()
