前言
之前学习反序列化漏洞,全是参考网上的乱七八糟的文章,然后我觉得反序列化漏洞是非常难的一部分内容。今天我看了phith0n师傅写的文章[1],我觉得写的非常棒。本文章的内容几乎均来自phith0n师傅,部分加了一些自己理解的部分。文章里面还有一些感想,这里给大家分享一下。
学习方式的探索⭐️⭐️⭐️
有时候想要学习⼀个东⻄,网上搜索⼀下,发现有教程,于是跟着做⼀遍。这样⼀来,你会发现网上大部分Java反序列化”教程”、“⼊门”通常上来先了解Java反序列化是什么,然后很快开始讲
CommonsCollections
,就好像刚知道C语言语法的同学⽴马继续学习Linux内核,我是⼗分不建议这样做的,除⾮你有非常强的理解能⼒。学习需要聪明⼀点,并独⽴思考问题。我很少参照别⼈的文章来学习,这样你学的东⻄是⼆手的,有时候连⼆手都不是,⽂章原作者也可能是参考另一篇⽂章写的。
我的建议是从⽂档和源码开始学,实在有压⼒可以参考⼀些⻛评较好的书籍或技术博客。
当然,有时候你并不知道怎样是对的怎样是错的。⽐如你作为⼀个Java反序列化漏洞的初学者,你并不知道应该先学习
URLDNS
还是CommonsCollections
,也许你连URLDNS
的名字都没听过,而你看到⽹上⼤部分文章都是介绍CommonsCollections
的,⾃然也就去学习这个了。所以有些弯路确实是避免不了的,不过如果你在学习的道路上更加富有探索精神,看到
ysoserial
这种项⽬时多问问⾃己“其他的Gadget是做什么的”,对于未知的事物原理充满好奇喜欢翻翻源码,应该是可以少⾛⼀些弯路的。
简化版CC1链Demo
这里先给出phith0n师傅独创的简化版CommonsCollections1
利用链[2],如下:
1 | package com.x2n.deserialization; |
运行上述的代码发现可以成功弹出计算器。
下面介绍一下整个过程中涉及的几个接口和类。
TransformedMap
TransformedMap
用于对Java标准数据结构Map
做一个修饰,被修饰过的Map
在添加新的元素时,将可以执行一个回调。我们通过下面这行代码对innerMap
进行修饰,传出的outerMap
即是修饰后的Map
。
1 | Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer); |
其中,keyTransformer
是处理新元素的key
的回调,valueTransformer
是处理新元素的value
的回调。我们这里所说的“回调”,并不是传统意义上的一个回调函数,而是一个实现了Transformer
接口的类。
Transformer
Transformer
是一个接口,它只有一个待实现的方法:
1 | public interface Transformer { |
TransformedMap
在转换Map
的新元素时,就会调用transform
方法,这个过程就类似在调用一个“回调函数”,这个回调的参数是原始对象(Object对象)。
ConstantTransformer
ConstantTransformer
是实现了Transformer
接口的一个类,它的过程就是在构造函数的时候传入一个对象,并在transform
方法将这个对象再返回。
1 | public ConstantTransformer(Object constantToReturn) { |
所以它的作用其实就是包装任意一个对象,在执行回调时返回这个对象,进而方便后续操作。
InvokerTransformer
InvokerTransformer
是实现了Transformer
接口的一个类,这个类可以用来执行任意方法,这也是反序列化能执行任意代码的关键。
在实例化这个InvokerTransformer
时,需要传入三个参数:
-
第一个参数就是待执行的方法名
-
第二个参数是这个函数的参数列表的参数类型
-
第三个参数是传给这个函数的参数列表
1 | public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { |
后面的回调transform
方法,就是执行了input
对象的iMethodName
方法。
1 | public Object transform(Object input) { |
ChainedTransformer
ChainedTransformer
也是实现了Transformer
接口的一个类,它的作用是将内部的多个Transformer
串在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入,示意图如下:
它的代码也比较简单,如下:
1 | public ChainedTransformer(Transformer[] transformers) { |
理解简化版Demo
了解了前面的几个Transformer
的意义以后,再回头看一看我们的demo的代码,这就比较好理解了。
先看前两段代码:
1 | Transformer[] transformers = new Transformer[]{ |
创建了一个ChainedTransformer
对象,根据前面说的,ChainedTransformer
的作用是将内部的多个Transformer
串在一起。ChainedTransformer
需要的参数是Transformer[]
,我们创建了Transformer[]
,其中包含了两个Transformer
:
- 第一个是
ConstantTransformer
,直接返回当前环境的Runtime
对象; - 第二个是
InvokerTransformer
,执行Runtime
对象的exec
方法,参数是/System/Applications/Calculator.app/Contents/MacOS/Calculator
。
这个transformerChain
只是一系列回调,我们需要用其来包装innerMap
,使用前面说到的TransformedMap.decorate
:
1 | Map innerMap = new HashMap(); |
最后,怎么触发回调呢?就是向Map
中放入一个新的元素:
1 | outerMap.put("xxx","tttt"); |
上面的代码执行demo,它只是一个用来在本地测试的类。在实际反序列化漏洞中,我们需要将上面最终生成的outerMap
对象变成一个序列化流。
用TransformedMap编写真正的POC[3]
前面我们说到了CommonsCollections1
的简化版的demo,但离一个真正可利用的POC还有很大的距离,现在我们着手对其进行修改。
AnnotationInvocationHandler
我们前面说过,触发这个漏洞的核心,在于我们需要向Map
中加入一个新的元素。在demo中,我们可以手工执行outerMap.put("xxx","tttt");
来触发漏洞,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObjection
逻辑里有类似的写入操作。
这个类就是sun.reflect.annotation.AnnotationInvocationHandler
,我们查看它的readObject
方法(这是8u71以前的代码,8u71以后做了一些修改,这个后面会说):
1 | private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { |
核心逻辑就是Map.Entry<String, Object> memberValue : memberValues.entrySet()
和 memberValue.setValue(...)
。
memberValues
就是反序列化后得到的Map
,也是经过了TransformedMap
修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用setValue
设置值的时候就会触发TransformedMap
里注册的Transform
,进而执行我们为其精心设计的任意代码。
所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler
对象,并将前面构造的HashMap
设置进来:
1 | Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
这里因为sun.reflect.annotation.AnnotationInvocationHandler
是在JDK内部的类,不能直接使用new
来实例化。我使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了。
AnnotationInvocationHandler
类的构造函数有两个参数,第一个参数是一个Annotation
类;第二个是参数就是前面构造的Map。
这里大家可以思考一下:什么是
Annotation
类?为什么我这里需要使用Retention.class
?
为什么需要使用反射?
上一小节我们构造了一个AnnotationInvocationHandler
对象,它就是我们反序列化利用链的起点了。我
们通过如下代码将这个对象生成序列化流:
1 | ByteArrayOutputStream barr = new ByteArrayOutputStream(); |
我将这几段代码拼接到demo代码的后面,组成一个完整的POC。
我们试着运行这个POC,看看能否生成序列化数据流:
运行代码发现报错了,在writeObject的时候出现异常了: java.io.NotSerializableException: java.lang.Runtime
。
原因是,Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable
接口。而我们最早传给ConstantTransformer
的是Runtime.getRuntime()
,Runtime
类是没有实现java.io.Serializable
接口的,所以不允许被序列化。
那么,如何避免这个错误呢?我们可以变通一下,看前面p牛的《Java安全漫谈 - 反射篇》[4]的同学应该知道,我们可以通过反射来获取到当前上下文中的Runtime
对象,而不需要直接使用这个类。
1 | Method f = Runtime.class.getMethod("getRuntime"); |
转换成Transformer
的写法就是如下:
1 | Transformer[] transformers = new Transformer[]{ |
其实和demo最大的区别就是将Runtime.getRuntime()
换成了Runtime.class
,前者是一个java.lang.Runtime
对象,后者是一个java.lang.Class
对象。Class类有实现Serializable
接口,所以可以被序列化。
为什么仍然无法触发漏洞?
修改Transformer
数组后再次运行,发现这次没有报错异常,而且输出了序列化后的数据流,但是反序列化时仍然没弹出计算器,这是为什么?
这个实际上和AnnotationInvocationHandler
类的逻辑有关,我们可以动态调试就会发现,在AnnotationInvocationHandler:readObject
的逻辑中,有一个if
语句对var7
进行判断,只有在其不是null
的时候才会进入里面执行setValue
,否则不会进入也就不会触发漏洞。
断点调试代码:
那么如何让这个var7
不为null
呢?这一块我就不详细分析了,还会涉及到Java
注释相关的技术。直接给出两个条件:
-
sun.reflect.annotation.AnnotationInvocationHandler
构造函数的第一个参数必须是Annotation
的子类,且其中必须含有至少一个方法,假设方法名是X -
被
TransformedMap.decorate
修饰的Map
中必须有一个键名为X的元素。
所以,这样就解释了为什么我前面用到Retention.class
,因为Retention
有一个方法,名为value
,所以,为了再满足第二个条件,我需要给Map
中放入一个key
是value的元素[5]:
1 | innerMap.put("value", "xxxx") |
成功弹出计算器。
为什么Java高版本无法利用?
已经成功弹出计算器了,但拿着这串序列化流,跑到服务器上进行反序列化时就会发现,又无法成功执行命令了。这又是为什么呢?
前文说了,我们是在Java 8u71以前的版本上进行测试的,我使用的Java版本为8u65,在8u71以后大概是2015年12月的时候,Java官方修改了sun.reflect.annotation.AnnotationInvocationHandler
的readObject
函数:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d
对于这次修改,有些文章说是因为没有了setValue
,其实原因和setValue
关系不大。改动后,不再直接使用反序列化得到的Map
对象,而是新建了一个LinkedHashMap
对象,并将原来的键值添加进去。所以,后续对Map
的操作都是基于这个新的LinkedHashMap
对象,而原来我们精心构造的Map
不再执行set
或put
操作,也就不会触发RCE
了。
总结
我们将给出的demo扩展成为了一个真实可利用的POC,完整的代码如下:
1 | package com.x2n.deserialization; |
但是这个Payload有一定局限性,在Java 8u71以后的版本中,由于sun.reflect.annotation.AnnotationInvocationHandler
发生了变化导致不再可用,原因前文也说了。我们查看ysoserial
的代码,发现它没有用到我demo中的TransformedMap
,而是改用了LazyMap
。有的同学包括我,之前以为这就是在解决CommonCollections1
这个利用链在高版本Java中不可用的问题,其实不然,即使使用LazyMap
仍然无法在高版本的Java中使用这条利用链,主要原因还是出在sun.reflect.annotation.AnnotationInvocationHandler
这个类的修改上,不过本篇文章先不讲了。
下一篇文章,再给大家分析如何破局。