0x00、前言
针对CC链的分析,主要还是以逆向思维为主(从结果分析过程),从中尽可能学习出问题的地方在哪,哪里调用的这个带问题的地方,一步步思考。
0x01、Apache Commons Collections描述
CC链即Commons Collections利用链,主要针对Commons Collections组件发现的利用链。
Apache Commons是Apache软件基金会的项目。Commons的目的是提供可重用的、开源的Java代码。
Apache Commons提供了很多工具类库,他们几乎不依赖其他第三方的类库,接口稳定,集成简单,可以大大提高编码效率和代码质量。
Apache Commons Collections 是对 java.util.Collection 的扩展。
Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
目前 Collections 包有两个 commons-collections 和commons-collections4,commons-collections 最新版本是3.2.2,3系列版本也是只有3.2.2是安全的,不支持泛型,目前官方已不在维护。collections4 目前最新版本是4.4,其中4.0是存在漏洞,最低要求 Java8 以上。相对于 collections 来说完全支持 Java8 的特性并且支持泛型,该版本无法兼容旧有版本,于是为了避免冲突改名为 collections4。推荐直接使用该版本。(注:两个版本可以共存,使用时需要注意)
CC1链分为两条链,一条为TransformedMap
(也是最初的CC1链),一条为LazyMap
,其中有的分析把TransformedMap
链单独分出来作为CC链,把LazyMap
链作为CC1链。也可以作为CC1的两条链,一起学习下。
0x02、环境准备
java版本:jdk8u66(cc1链要求java版本小于jdk8u71,不得不说这个版本确实有点老了)
jdk官方下载链接:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html,找到对应版本低于8u71的就行。
添加Maven项目
新建Maven项目——>pom.xml添加Commons Collections组件依赖,3系列除3.2.2版本外,其余均收该漏洞影响,添加3系列其中一个版本即可
<dependencies> |
导入sun包
因为cc1链需要用到sun包中的类,sun包在jdk中的代码是通过class文件反编译来的,为.class文件,查看不到源码.java文件,不便于调试,且直接观看class文件的阅读性跟java文件有出入,所以下载jdk源码sun包导入,便可查看到java源码文件,方便调试阅读
步骤:
下载jdk源码:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk
按网上步骤解压出src文件夹,再把/src/share/classes/sun文件夹放到解压出来的src文件夹根目录
在项目结构中对应jdk版本下添加包即可如下图
随后在分析过程对应maven里Commons Collections包的class文件上方提示下载源代码,下载就可以看到Commons Collections包的java源码
0x03、利用链接口及实现类
CC1链的利用点为Commons Collections
包中的Transformer
接口,通过下面后半段的利用链可以了解Transformer
接口相关涉及实现它的ChainedTransformer
类、ConstantTransformer
类、InvokerTransformer
类。
yso-cc1链后半段利用链 |
Transformer接口
//我理解为 转换器接口Transformer
接口只有一个public Object transform(Object input)
方法,返回一个被转换的Object对象
Transformer接口实现的类,包括利用链中的关键的类,这些类都实现了Transformer
以及序列化serializable
。
ChainedTransformer类
//我理解为 链转换类ChainedTransformer
的作用为链式调用,将传入的Transformer
数组依次调用每一个Transformer
的transform
方法,将第一个的转换返回Object
对象的作为下次循环的的输入对象在Transformer
的transform
中被调用,以此循环。
ConstantTransformer
//我理解为 对象转换类
这里定义了一个iConstant
对象private final Object iConstant;
作用就是接受任何传进来的对象,并转换成定义的iConstant
对象返回,相对起来很好理解。
InvokerTransformer
//我理解为 调用转换类InvokerTransformer
类的作用为获取调用的方法名、参数类型、函数的参数列表
调用的transform
获取了对象的类,方法、并返回执行方法,类、方法、参数均为输入的对象,可控,因此该类是cc1链的最重点的执行部分。
通过正常反射执行Runtime.exec()
方法
通过InvokerTransformer
类执行Runtime.exec()
方法
poc-demo:
public static void main(String[] args) throws Exception { |
这下很好理解InvokerTransformer
这个类的就是为了获取对象以及方法和执行参数,且该对象可控。
0x04、CC1链分析
从利用链接口和实现类可知InvokerTransformer
为最后最重要的执行类,因此该链的分析思路就相对清晰,反查哪些类实现了InvokerTransformer.transform()
方法,输入源为反序列化readObject
,也就是先搜索最后调用了InvokerTransformer.transform(Object)
方法,且实现serializable
序列化,以readObject
为输入源的类及方法。
搜索结果有21个,除去自身测试的,还有20个,满足上述条件(调用transform(Object)
、实现了反序列化接口serializable
)的有6个类。
共8个方法
先学习分析一下TransformedMap链和LazyMap链,也就是CC1的两条链。
TransformedMap链
分析
TransformedMap
类作用是对Map进行装饰,通过keyTransformer
、valueTransformer
分别对输入的key
、value
通过transform()
方法进行修饰,查看构造函数,接受三个参数(Map
的对象、Transformer
的两个对象分别为key
、value
。),将接受的key,value
对象转换为本类的Transformer
对象
可调用静态方法(map类型)decorate
对新Map
对象进行回调,对传入新的Map
进行修饰,也就是说可以通过decorate
方法去获取TransformedMap
的对象
简单了解TransformedMap
类后,现在回到利用链本身来,找到可以实现的transform(Object)
的地方
三个方法
- transformKey(Object object)
- transformValue(Object object)
- checkSetValue(Object value)
其中transformKey
、transformValue
两个方法分别获取从TransformedMap
类的构造方法中传入Transformer keyTransformer
、Transformer valueTransformer
,再分别调用keyTransformer.transform(object)
、valueTransformer.transform(object)
//transformKey方法 |
查看这两个方法都在哪调用
这两个方法都在put方法中进行调用,参数就为两个Object
对象,传递任意一个参数都可调用对应的transformKey/Value
方法,最后put放入key
和value
。
利用思路:
因为TransformedMap
中构造函数、transformKey
、transformValue
方法都是protected
受保护的,无法直接声明使用,但可调用decorate
公共方法进行回调向构造方法传参,通过transformKey
或者transformValue
传递其中任意一个参数为InvokerTransformer
对象,最后通过TransformedMap
的put
方法传入恶意函数对象达到执行命令。
即:
第一步,创建构造参数(Map
的对象、Transformer
的两个对象分别为key
、value
),其中key或者value可以随意创造其中一个为InvokerTransformer
类对象即可
第二步,通过decorate
公共方法调用构造方法创建TransformedMap
对象
第三步,调用TransformedMap
的put
方法,传递恶意函数对象,对其中key和value参数随便传入一个即可
利用方法:
但上面这两个方法都在本类Put方法进行调用,也未在其他类中进行调用,同时使用到了InvokerTransformer
、Map
两个类的对象,因此无法通过readObject反序列化达到命令执行的目的。
注:反序列化的序列化对象只能为单个对象,无法对多个对象进行序列化。
接下来看第三个方法:checkSetValue(Object value)
同样也是返回构造方法传入的Transformer valueTransformer
,然后调用transform(Object)
,但是由于是protected
受保护,因此无法像另外两个方法一样直接调用。
再看该方法有没有其他类对其调用
发现除了本类以外的父类AbstractInputCheckedMapDecorator
中的内部类MapEntry
的setValue()
方法对其进行了调用,该类
static class MapEntry extends AbstractMapEntryDecorator { |
官方对该类的描述翻译:
一个抽象基类,简化了创建地图装饰器的任务。
MapAPI很难正确修饰,并且涉及实现许多不同的类。这个类的存在是为了提供一个更简单的API。
提供了特殊的钩子方法,当对象被添加到地图时调用这些方法。通过重写这些方法,可以验证或操纵输入。除了主要的映射方法,entrySet也会受到影响,这是编写映射实现最困难的部分。
该类是包范围的,在将来的Commons Collections版本中可能会被撤回或替换
实现调用
前面步骤跟另外两个方法一样,由于不能直接调用,因此重点就在for循环里面,通过遍历HashMap
键值对(entrySet()
为hashMap
映射识图,能返回map
键值对),调用setValue()
方法
public static void main(String[] args) throws Exception { |
现在发现父类AbstractInputCheckedMapDecorator
中的内部类MapEntry
的setValue()
方法对其进行了调用,下一个思路就是找到实现序列化接口并且能调用setValue()
方法的
找到42个方法,其中AnnotationInvocationHandler
类实现了序列化接口,同时重写了readObject
方法,并且setValue()
在重写的readObject
方法中进行调用
其中构造函数获取两个参数,一个class类型Class type
,一个Map类型Map memberValues
,然后进行了type的类型判断,最后返回给变量
序列化前半段也是通过getInstance
方法对type类型进行判断,判断type的类型是否为注解类的方法类型名,是的话便通过memberTypes()
方法获取其成员方法名和返回方法,存在Map类型的memberTypes
对象中
后半段也是最主要的for循环部分,遍历memberValues
也就是构造方法传入的第二个参数(传入TransformedMap[]数组),循环获取键名,通过memberTypes.get(name)
判断键名是否是注解类(Annotation类,全名java.lang.annotation.Annotation)的方法类型名,如果是的话就获取键值给value
变量,这里通过键名设置注解类(Annotation类)的实例方法名即可通过条件判断。
接下来继续判断if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)
意思判断value的值如果不是注解类的方法或者键名的值是ExceptionProxy的实例,则通过memberValue.setValue
方法修改该键名的值,这里只用键值不是注解类(Annotation类)的实例方法名即可满足条件判断,然后调用memberValue.setValue
方法达到目的
可以找到注解类Annotation的实现方法(找到引用的java.lang.annotation.Annotation,Ctrl+Alt+鼠标左键点击Annotation即可找到对应的实现方法),找到其中对应有成员的任意方法
比如Generated注释类,可以put value方法名。(实际测试过程中找到的实现方法有大部分还是不能触发,只有部分类和方法才行)
现在利用链就完整了,入口点为AnnotationInvocationHandler
类的反序列化readObject
方法,一直到最后调用InvokerTransformer.transform(Object)
方法
完整利用链
TransformedMap CC1链
ObjectInputStream.readObject() |
其中通过上述学习还没涉及到ChainedTransformer.transform()
这个环节的利用方法,在构造Poc中可以学习下利用方法和原理
构造POC
理思路
1、入口点在为AnnotationInvocationHandler
类的反序列化readObject
方法中调用setValue()
方法,但AnnotationInvocationHandler
这个类未声明Public,只有通过反射进行调用。
通过反射调用AnnotationInvocationHandler
类对象,并且调用构造方法,然后通过实例化向构造方法传参
//获取AnnotationInvocationHandler类对象 |
2、接下来久要对传参内容进行确定,传入什么,其中a为Class类对象,b为Map对象,通过反序列化调用,从利用链的分析知道a传入的为注解类Annotation
类,可以找到的Generated
类的value
名称,同时建立HashMap
,并put键名为value
,键值为任意即可绕过for循环的判断
Map map = new HashMap(); |
b为Map对象,并且该参数会执行调用到AnnotationInvocationHandler
类readObject
方法中的AbstractMapEntryDecorator.MapEntry.setValue()
,从而在setValue()
方法中调用到TransformedMap.checkSetValue()
方法,最后返回调用到TransformedMap
中valueTransformer.transform()
方法,valueTransformer
为TransformedMap
构造函数的第三个参数即传入InvokerTransformer
类对象,Runtime
对象作为transform(Object)
参数传入的值
但由于Runtime对象未实现序列化接口,无法序列化,并且valueTransformer.transform(Object)
中的Object实际并不可控设置为Runtime对象值。因此利用到了ChainedTransformer
类,也是上文学习中未提及实际利用方法的类,该类传入Transformer数组,并由transform方法实现循环调用。
因为Runtime
类未实现序列化接口,只有通过反射实现对它的调用,即Runtime.class
,再由于这个循环会有一个初识的transform(Object),其中的Object在第一次循环的时候是由调用ChainedTransformer.transform(Object)
传入的(传入的重写的反序列化方法中setValue()中的值),并没有实际传入的类对象。
此时通过调用ConstantTransformer
类来解决这个问题,一来可以通过它来实现返回Runtime.class
对象,二来通过把他设置为TransformedMap
数组的首位来规避初始调用的Object
的问题,因为ConstantTransformer
的transform(Object)
不管输入什么都返回构造方法中的对象,然后再通过反射构建Runtime
对象并调用exec
方法,最后执行命令。
Transformer[] transformers = new Transformer[]{ |
在理解ConstantTransformer
的transform(Object)
方法的实际原理时也挺绕,可以试着把上面的执行效果带入到该方法中,能更好的理解如何实现最后调用到InvokerTransformer
方法并传入Runtime
对象
通过一次一次循环把对应Object
输出到下次作为输入就能理解怎么实现的了
object = new ConstantTransformer(Runtime.class).transform(123); |
3、在实现了2步骤的一长段的利用链的相关调用,最后就是传递的b就是Transformer transformerChain = new ChainedTransformer(transformers);
中的transformerChain
数组
//获取AnnotationInvocationHandler类对象 |
4、最后就是序列化该AnnotationInvocationHandler
对象,反序列化时触发漏洞,得到最终POC
public static void main(String[] args) throws Exception { |
LazyMap链
分析
接下来是LazyMap链,CC1的另一条链,也是ysoserial中利用CC1的链。在分析InvokerTransformer
类时说到该类为最终的执行类,找到调用InvokerTransformer.transform(Object)
方法的类,其中有上文分析的TransformedMap类,还有个就是LazyMap类
该类通过构造方法传入对象并赋值给factory
调用了transform()
方法的get
方法
get
方法中会判断参数key
是否为HashMap
中Map
的内容,如果不是Map
中的内容,就创建一个value
作为key
的值放入Map
中,给value赋值时调用了transform()
方法。
该get()
方法在AnnotationInvocationHandler
类的invoke
方法中可调用,通过构造方法传入LazyMap
对象然后在invoke
方法中调用到LazyMap.get()
方法
invoke方法的调用并不在该类重写的readObject
方法中,因此入口点就有点变化,AnnotationInvocationHandler
类实现了InvocationHandler
动态类,这里调用invoke
方法就涉及到动态代理
动态代理InvocationHandler
:
每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用
实现方法:
[接口类] proxyMap=(接口类)Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invh) |
例如使用Map对象进行动态代理:
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},invocationHandler); |
其中涉及到Proxy
代理类,并通过newProxyInstance()
实现动态代理。newProxyInstance
类:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invh); |
三个参数分别表示:目标对象所属类的加载器、目标对象实现的接口数组、调用接口时触发的对应方法
实现demo:
public class Proxydemo { |
可以看到map对象每执行一次方法,便会调用执行一次invoke方法,invoke方法也可以起拦截器的作用。
简单了解了动态代理的用法,接下来回到LazyMap类的调用LazyMap
的get
方法在AnnotationInvocationHandler
类的invoke
方法中可调用
因此入口点就在实现动态类调用到LazyMap的invoke方法。
完整利用链
LazyMap CC1链
ObjectInputStream.readObject() |
构造POC
POC的前半段依旧采用TransformedMap利用链的代码
Transformer[] transformers = new Transformer[]{ |
将传参入口变更至LazyMap类中
Map Lmap = LazyMap.decorate(map, transformerChain); |
同TransformedMap
利用链一样通过反射调用AnnotationInvocationHandler
类,然后调用InvocationHandler代理调用AnnotationInvocationHandler
类的构造函数传入参数
//反射调用AnnotationInvocationHandler类 |
然后创建proxy
代理对象,参数分别为Map加载器
、Map类数组
、InvocationHandler对象invohandler
Map proxymap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},invohandler); |
再通过代理调用代理对象,执行AnnotationInvocationHandler.invoke
方法
InvocationHandler invohandlerproxy=(InvocationHandler)declaredConstructor.newInstance(Generated.class,proxymap); |
最后就是对InvocationHandler
对象进行序列化,再反序列化触发漏洞
得到最终POC:
public static void main(String[] args) throws Exception { |
0x05、总结
整体来说就是找到能够执行恶意函数的方法,然后一步步查看调用链,最后找到反序列化入口,cc1链看下来就比URL链复杂很多,链路调用步骤多了很多,有些链的类还需要明白怎么实现的,LazyMap加了动态代理然后调用链更绕。
分析单从完整利用链来推导相对容易很多,但会错过很多细节以及实现原理,可能会导致当时看完明白了过后过了段时间再回想就没法理清完整的利用链,这次分析的过程花费了不少的时间,中间有很多原理细节琢磨了有点久,只是记了我认为需要细理解的地方,可能还有些小细节没些或者被跳过了没注意,后面再看到的话再补充进来吧。
0x06、参考链接
https://paper.seebug.org/1242/#commonscollections-1
P牛-Java漫谈