什么是反序列化
序列化,是指将内存中的某个对象压缩成字节流的形式,而反序列化,则是将字节流转化成内存中的对象。Java序列化和反序列化处理是基于Java框架的Web应用中比较重要的功能。因为在网络中,无论相互间发送何种类型的数据,在网络中实际上都是以二进制序列的形式传输的。为此,发送发必须将要发送的Java对象序列化为字节流,接收方则需要将字节流再反序列化,还原得到java对象才能实现正常通信。当攻击者精心构造的字节流被反序列化为恶意对象时,就会造成一系列安全问题。反序列化漏洞就是,暴露或者间接暴露反序列化API,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码。
在java原生的api中,序列化的过程由ObjectOutputStram
类的writeObject()
方法实现,反序列化则由ObjectInputStream
类的readObject()
方法实现。
Java序列化通过ObjectOutputStream
类的writeObject()
方法完成,能够被序列化的类必须要实现Serizlizable
接口或者Externalizable
接口。Serializable接⼝是⼀个标记接⼝,其中不包含任何⽅法。Externalizable接⼝是Serializable⼦类,其中包含writeExternal()
和readExternal()
⽅法, 分别在序列化 和反序列化的时候⾃动调⽤。
反序列化不安全的原因
Java反序列化通过ObjectInputStream
类的readObject()
⽅法实现。在反序列化的过程中,⼀个字节流将按照⼆进制结构被序列化成⼀个对象。当开发者重写readObject
⽅法或readExternal
⽅法时, 其中如果 隐藏有⼀些危险的操作且未对正在进⾏序列化的字节流进⾏充分的检测时,则会成为反序列化漏洞的触发点。
相关代码分析
序列化类的对象需要满足两个条件:
- 该类必须实现
java.io.Serializable
接口
跟进该接口可以发现它是一个空接口,说明其作用只是为了在序列化和反序列化中做一个类型判断。(非遵循非必要原则), 不需要序列化的类就可以不用序列化。
- 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须标注是短暂的,比如static,transient修饰的变量不可被序列化。
- 如何序列化类
java原生实现了一套序列化的机制,它让我们不需要额外编写代码,只需要实现java.io.Serializable
接口,并调用ObjectOutputStream
类的writeObject
方法即可
1 | public static void test1() throws IOException { |
跟进writeObject函数,我们通过阅读它的注释可以知道,在序列化的过程当中,是针对对象本身,而非针对类的,因此静态属性是不参与序列化和反序列化过程的。另外,如果属性本身声明了transient关键字,也会被忽略。但是如果某对象继承了A类,那么A类当中的对象的对象属性也是会被序列化和反序列化的(前提是A类也实现了Serizlizable
接口)
案例1
1 | public class Student implements Serializable { |
案例2
1 | public class Serialize implements Serializable{ |
我们知道了由于重写了writeObject函数。并且往里面加了s.writeObject("This is writeObject! ");
(额外要序列化得数据,也即是在序列化对象时插入一些自定义数据,在反序列的时候使用readObject
将其读取出来。那么如果将写入的数据换成了恶意对象呢,那么就会造成恶意的反序列化,)
案例3
1 | public class Student3 implements Serializable { |
代码中用户自定义了readObject
函数。使其执行了恶意的代码 Runtime.getRuntime().exec("calc");
。从而执行了系统命令。为何能执行上面的代码?
正常业务以及组件中一般不会放这些代码,需要用到两个或多个常用的组件构造一个利用链,能从readObject
开始到经过有限步骤最后执行我们的恶意方法或命令结束。当应用程序调用了被序列化对象的readObject()
方法,且被序列化对象重写了readObject()
方法,方法中可以执行任意代码时,造成了远程代码执行漏洞。
反序列化链分析
URLDNS链
URLDNS是ysoserial⼯具⽤于检测是否存在Java反序列化漏洞的⼀个利⽤链,通过URLDNS利⽤链可以 发起⼀次DNS查询请求,从⽽可以验证⽬标站点是否存在反序列化漏洞,并且该利⽤链任何不需要第三 ⽅依赖,也没有JDK版本的限制。但是URLDNS利⽤链也只能⽤于发起DNS查询请求,也不能做其他事 情,因此URLDNS链更多的是⽤于POC检测。
URLDNS链的基本工作流程如下
- 这个链是HashMap反序列化时(执行
readObject
方法时)会从序列化流中读取它在序列化时写入的Node数组。(实现Map.Entry
接口和Map的内部类,Entry是描述一组键值对),再循环赋值给HashMap,来还原序列化之前的数据。赋值的时候调用到putVal
方法,其中第一个参数是原HashMap对象中key(键)的hash值,计算这个hash值调用到key自己的hashcode
方法。 - URL对象的
hashCode
函数会在其hash值为 -1 时调⽤默认URLStreamHandler
的hashCode
⽅法重新计算hash值,这个⽅法计算hash值时会调⽤getHostAddress
,getHostAddress
⾥调⽤InetAddress
类的getByName
,getByName
本来功能就是解析域名,最后触发DNS解析。
URLDNS链的特点和条件:
- 原生JDK中就有此链,并且不限版本,不限组件。简单理解,是因为HashMap从功能原理上来说,就是按key的hash值存储数据的散列值,且计算URL的hash值时就是需要其主机地址的。(非必须,但是最好是有,减少哈希碰撞)。
- 此链比较适用于验证目标应用程序是否有反序列化漏洞或者是否出网。
- 恶意序列化数据需要一个
Hashmap
,并且key值是url
对象,其hashcode
是 -1.
1 | public class URLDNSTest1 { |
分析过程如下:1.以HashMap的put方法为入口。
2.进入put方法后,继续以此进入hash()方法。
3.调式发现,需要跟进hashmap的hashcode方法,于是跟进hashcode()方法。但是要注意此处应该是url包下的方法。
在url包下面,默认的hashcode是-1。
4.当hashcode为-1时,需要重新计算hashcode,这个时候通过查看方法。
发现
关键动作:调用getHostAddress()方法查看当前url的主机ip,为请求url做准备。最后发现请求成功。
链总结
1 | HashMap.readObject |
CC3链
Apache commons-collectionis组件反序列化漏洞的反射链也称为CC链,自从apache commons-collections组件爆出第一个java反序列化漏洞后,就像打开了java安全的新大门一样,之后很多java中间件相继都爆出了反序列化漏洞。CC链的原理就是利用反射获取类,放到readObject方法。
在挖掘反序列化漏洞时比较常用的利用工具ysoserial
就使用LazpMap
类的利用链。
相关知识
InvokeTransformer
继承自Transformer类,这个类有一个函数叫transform,它的作用很简单,会把当前类的ImethodName
和IparamTypes
进行反射调用。
在java反射中,我们可以通过反射来调用exec方法
1 | public class CC1Test { |
我们同样也可以使用Transformer调用exec函数。
1 | public class CC1Test2 { |
POC构造实现反序列化调用EXEC
LazyMap
LazyMap
本质上也是一个Map,它允许只当一个Transformer作为它的工厂类。
工厂类的意思是,当进行Map操作时,这个工厂类会对它进行修饰。(使用工厂类的transform函数)
同时它的下面有get方法,用来调用工厂类的transform函数
AnnotationInvocationHandler
最后一步,我们需要寻找在重载了readObject
函数中,会调用map属性get方法的类。这个类就是AnnotationInvocationHandler
。首先看它的类声明,可以确定存在对应的map属性。
接下来查看它的invoke方法,可以看到调用了get方法。
这里有一个问题就是AnnotationInvocationHandler
在它重载的readObject
函数当中,并没有调用invoke方法,为什么它是可以利用的。
这是因为AnnotationInvocationHandler
是动态代理类,这意味着我们可以使用该类包裹我们的LazyMap,这样就能触发它的invoke函数。
在AnnotationInvocationHandler
的ReadObject
中,它直接操作了自身的map。
接下来只需要把这几个部分拼装起来就好了
1 | public class CC1Test3 { |
JDK8u71后⽆法复现该漏洞,核⼼原因是,8u71以后,AnnotationInvocationHandler
的ReadObject
z中,不再直接操作我们给的Map,⽽是新创建了⼀个LinkedHashMap
,导致⽆法触发后⾯的payload。