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()