java反序列化基础
闲下来研究一下反序列化(虽然目前也不算是闲下来哩),但是总不能一直当脚本小子哩。下面的内容不是完全原创的,借鉴的别人的,毕竟是学习笔记。
序列化与反序列化
其实在之前学习php的时候就学过了
1 | 序列化:对象 -> 字符串 |
1 | 序列化的好处 |
几种创建的序列化和反序列化协议
1 | XML&SOAP |
代码展示
类文件:Person.java
1 | import java.io.Serializable; |
序列化文件 SerializationTest.java
1 | package src; |
反序列化文件 UnserializeTest.java
1 | package src; |
运行结果
解析
- SerializationTest.java
这里我们将代码进行了封装,将序列化功能封装进了 serialize这个方法里面,在序列化当中,我们通过这个FileOutputStream输出流对象,将序列化的对象输出到ser.bin当中。再调用 oos 的writeObject方法,将对象进行序列化操作。
1 | ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); |
- UnserializeTest.java
进行反序列化
1 | ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); |
Serializable 接口
(1) 序列化类的属性没有实现 Serializable那么在序列化就会报错
只有实现 了Serializable或者 Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。
1 | public interface Serializable { |
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。
(2) 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
(3)一个实现 Serializable接口的子类也是可以被序列化的。
(4) 静态成员变量是不能被序列化
序列化是针对对象属性的,而静态成员变量是属于类的。
(5) transient 标识的对象成员变量不参与序列化
这里我们可以动手实操一下,将 Person.java中的name
加上transient
的类型标识。加完之后再跑我们的序列化与反序列化的两个程序,修改过程与运行结果如图所示。
引例
网上找了个引例,加深我对代码的理解哩。
(1) 入口类的readObject
直接调用危险方法
先创建一个恶意类Person
1 | package src; |
1 | private void readObject(ObjectInputStream ois)throws IOException,ClassNotFoundException{ |
这段代码就是弹计算器的。
先运行序列化程序 ———— “SerializationTest.java”,再运行反序列化程序 ———— “UnserializeTest.java”
这时候就会弹出计算器,也就是calc.exe
,是不是帅的飞起哈哈。
这是黑客最理想的情况,但是这种情况几乎不会出现。
(2) 入口参数中包含可控类,该类有危险方法,readObject
时调用
(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject
时调用
(4) 构造函数/静态代码块等类加载时隐式执行
URLDNS实战
其实还是半懂不懂的,跟着大佬做一做实战。
出发点:URLDNS 在 Java 复杂的反序列化漏洞当中足够简单;URLDNS 就是 ysoserial 中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。
因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次 DNS 请求。
虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合我们在检测反序列化漏洞时使⽤。
使⽤ Java 内置的类构造,对第三⽅库没有依赖。
在⽬标没有回显的时候,能够通过 DNS 请求得知是否存在反序列化漏洞 URL 类,调用openConnection
方法,到此处的时候,其实openConnection
不是常见函数,就已经难以利用了。
我们先去到 ysoserial 的项目当中,去看看它是如何构造 URLDNS 链的。
1 | https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java |
1 | * Gadget Chain: |
URL 是由 HashMap 的put
方法产生的,所以我们先跟进put
方法当中。put
方法之后又是调用了hash
方法;hash
方法则是调用了hashcode
这一函数。
现在我们来实战分析一下,现在双击shift,输入hashmap.java来看看源码。
URL 是由 HashMap 的put
方法产生的,所以我们先跟进put
方法当中。put
方法之后又是调用了hash
方法;hash
方法则是调用了hashcode
这一函数。
我们看到这个hashCode
函数的变量名是 key;那这个 key 是啥啊?
噢 ~ 原来 key 是hash
这一方法传进的参数!那我们前面写的 key 不就是这个东东吗 ~!
也就是put方法传入了一个key,最后hash方法中又调用了key的hashcode方法
1 | hashmap.put(new URL("DNS生成的 URL,用dnslog就可以"),1); |
现在跟进URL,去看看 URL 跟进一堆之后的hashCode
方法是如何实现的。
跟进 URL,我们肯定是要去寻找 URL 调用的函数的函数(的函数,应该还有好几个的函数,就不写出来了,不然大家就晕了)的hashCode
方法。
在左边 Structure 直接寻找hashCode
方法,URL 中的hashCode
被handler
这一对象所调用,handler
又是URLStreamHandler
的抽象类。我们再去找URLStreamHandler
的hashCode
方法。
终于找到了,这个用于 URLDNS 的方法 ————getHostAddress
这⾥InetAddress.getByName(host)
的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。到这⾥就不必要再跟了。
所以,⾄此,整个 URLDNS 的Gadget其实清晰⼜简单:
1 | HashMap->readObject() |
复现
SerializationTest.java文件下添加如下代码
1 | HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>(); |
我们先把它序列化,按照道理来说,这个过程应该什么都不会发生对吧。
但是确实收到了dnslog信息
还是从原理角度分析,我们回到 URL 这个对象,回到hashCode
这里。
我们发现,当hashCode
的值不等于 -1 的时候,函数就会直接return hashCode
而不执行hashCode = handler.hashCode(this);
。而一开始定义 HashMap 类的时候hashCode
的值为 -1,便是发起了请求。
所以我们在没有反序列化的情况下面,就收到了 DNS 请求,这是不正确的。
但是这里就涉及到关于反射的一个庞大知识体系,那个博主在这里也没有讲,直接放出poc
SerializationTest.java
1 | package src; |
反序列化的文件无需更改
接着我们运行序列化文件,是收不到 DNS 请求的,而当我们运行反序列化的文件时候,可以收到请求,这就代表着我们的 URLDNS 链构造成功了。