Commons-Collections1原理分析
已经看了三遍CC链的经典POC了,每次看都有新收获,打算再看一遍,做一个详细的分析笔记。在这个过程中不仅回顾了一遍,以前比较模糊的内容都搞懂了,还是很有意义的。
前置知识
AbstractMapDecorator
首先 CC 库中提供了一个抽象类 org.apache.commons.collections.map.AbstractMapDecorator
,这个类是 Map 的扩展,并且从名字中可以知道,这是一个基础的装饰器,用来给 map
提供附加功能,被装饰的 map 存在该类的属性中,并且将所有的操作都转发给这个 map
。
这个类有很多实现类,各个类触发的方式不同,重点关注的是 TransformedMap
以及 LazyMap
。
TransformedMap
org.apache.commons.collections.map.TransformedMap
类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer
来定义,Transformer
在 TransformedMap
实例化时作为参数传入。
也就是说当 TransformedMap
内的 key
或者 value
发生变化时(例如调用 TransformedMap
的 put
方法时),就会触发相应参数的
Transformer
的 transform()
方法。
比如我写了这个文件,定义了两个Transformer
–keyTransformer
和valueTransformer
,它俩包含transform
方法。
使用TransformedMap.decorate()
进行包装后,调用map
的put
方法,会进到TransformedMap的put中(因为上文提到的原因)
可以看到,,先回执行key/value = transformKey(key/vlaue);
,然后才是return getMap().put(key, value);
进入transformKey()
方法
可以看到,方法内调用了相应Transformer
的transform()
方法,这就产生了宏观意义上的“回调”。
总结起来就是,当TransformedMap.decorate()
中参数一的key/value发生变化时,会对应“回调”参数二/参数三。
LazyMap
org.apache.commons.collections.map.LazyMap
与 TransformedMap
类似,不过差异是调用 get()
方法时如果传入的 key
不存在,则会触发相应参数的 Transformer
的 transform()
方法。
与 LazyMap
具有相同功能的是 org.apache.commons.collections.map.DefaultedMap
,同样是 get()
方法会触发 transform
方法。
Transformer
org.apache.commons.collections.Transformer
是一个接口,提供了一个 transform()
方法,用来定义具体的转换逻辑。方法接收 Object
类型的 input
,处理后将 Object
返回。
在 Commons Collection 3.2.1 中,程序提供了 14 个 Transformer 的实现类,用来实现不同的对 TransformedMap 中 key/value 进行修改的功能。
重点关注其中这几个实现类,InvokerTransformer
,ChainedTransformer
,ConstantTransformer
InvokerTransformer
这个实现类从 Commons Collections 3.0 引入,功能是使用反射执行任意方法,我们来看一下它的 transfrom
方法,是通过调用 input
的方法,并将方法返回结果作为处理结果进行返回。
调用需要的参数 iMethodName/iParamTypes
是在 InvokerTransformer
的构造函数中传入。这样我们就可以使用 InvokerTransformer
来执行方法
ChainedTransformer
它的作⽤是将内部的多个Transformer
串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊
这样就给了使用者链式调用多个 Transformer
分别处理对象的能力。
ConstantTransformer
org.apache.commons.collections.functors.ConstantTransformer
是一个返回固定常量的 Transformer
,在初始化时储存了一个 Object
,后续的调用时会直接返回这个 Object
。
这个类用于和 ChainedTransformer
配合,将其结果传入 InvokerTransformer
来调用我们指定的类的指定方法。
POC
TransformedMap POC
构造sink gadget和chain gadget
将上面的知识结合起来执行Runtime.getRuntime().exec("command");
Transformer[]
中的代码等价于:
1 | Method f = Runtime.class.getMethod("getRuntime"); |
为什么不直接构造new ConstantTransformer(Runtime.getRuntime())
呢?
原因是,Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable
接口。而我们最早传给ConstantTransformer
的是 Runtime.getRuntime()
,Runtime
类是没有实现 java.io.Serializable
接口的,所以不允许被序列化。
那么,如何避免这个错误呢?
我们可以变通一下,可以通过反射来获取到当前上下文中的Runtime
对象,而不需要直接使用这个类.
截止到这里,我们利用CC
库成功构造了 sink gadget
和 chain gadget
,接下来我们需要找到一个 kick-off gadget
:
一个类重写了 readObject
,在反序列化时可以改变 map
的值。
构造kick-off gadget
于是我们找到了 sun.reflect.annotation.AnnotationInvocationHandler
这个类。这个类实现了 InvocationHandler
接口,原本是用于 JDK
对于注解形式的动态代理。
看一下AnnotationInvocationHandler
的构造方法jdk1.8.0_05
jdk1.8.0_312
构造方法接收两个参数,第一个参数是 Annotation
实现类的 Class
对象,第二个参数是是一个 Map
, key
:String
、value
:Object
。
构造方法判断参数一Class<? extends Annotation>
有且只有一个父接口,并且是 Annotation.class
,才会将两个参数初始化在成员属性 type
和 memberValues
中。可以看到高版本JDK加了判断,低版本是直接赋值的。
这里的 memberValues
就是用来触发的 Map
。
接下来我们看一下这个类重写的 readObject
方法jdk1.8.0_05
< Java8u71
首先调用 AnnotationType.getInstance(this.type)
方法来获取 type
这个注解类对应的 AnnotationType
的对象,然后获取其 memberTypes 属性,这个属性是个 Map,存放这个注解中可以配置的值。
然后循环 this.memberValues
这个 Map
,获取其 Key
,如果注解类的 memberTypes
属性中存在与 this.memberValues
的 key
相同的属性,并且取得的值不是 ExceptionProxy
的实例也不是 memberValues
中值的实例,则取得其值,并调用 setValue
方法写入值。因为是setValue
,所以要将chain
写到TransformedMap.decorate
的valueTransformer
上。
用语言描述这些代码可能有些拗口,注解本质是一个继承了 Annotation
的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。通过代理对象调用自定义注解(接口)的方法,会最终调用 AnnotationInvocationHandler
的 invoke
方法。该方法会从 memberValues
这个 Map 中索引出对应的值。
而重写 readObject
方法,则给了程序传递注解值的能力了。
但是在高版本的jdk中将readObject
改了,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。
所以我们构造恶意 payload
的思路就清楚了:
- 构造一个
AnnotationInvocationHandler
实例,初始化时传入一个注解类和一个Map
,为了使readObject
可以顺利执行到var5.setValue
所在的if
语句中,需要让
这个Map
的key
中要有注解类中存在的属性,但是值不是对应的实例,也不是ExceptionProxy
对象。即:sun.reflect.annotation.AnnotationInvocationHandler
构造函数的第一个参数必须是Annotation
的子类、注解类(有属性的注解),且其中必须含有至少一个方法,假设方法名是X
- 被
TransformedMap.decorate
修饰的Map中必须有一个键名为X
的元素
- 这个
Map
由TransformedMap
封装,并调用自定义的ChainedTransformer
进行装饰。 ChainedTransformer
中写入多个Transformer
实现类,用于链式调用,完成恶意操作。
LazyMap POC
除了用 TransformedMap
,还可以用 LazyMap
来触发,之前提到过,LazyMap
通过 get()
方法获取不到 key
的时候触发 Transformer
。
我们发现 AnnotationInvocationHandler
的 invoke()
方法可以触发 memberValues
的 get
方法。
这里用到了动态代理,总结起来的一句话就是被动态代理的对象调用任意方法都会调用对应的InvocationHandler
的 invoke
方法。
那构造的思路的就有了,我们需要对sun.reflect.annotation.AnnotationInvocationHandler
对象进行Proxy;
代理后的对象叫做proxyMap
,但我们不能直接对其进行序列化,因为我们入口点是 sun.reflect.annotation.AnnotationInvocationHandler#readObject
,所以我们还需要再用AnnotationInvocationHandler
对这个proxyMap
进行包裹
1 | Map map = LazyMap.decorate(hashMap, chain); |
这样在反序列化时,会执行到AnnotationInvocationHandler#readObject
中的任意InvocationHandler
方法,都会通过代理触发InvocationHandler
的 invoke
方法,,继而触发invoke
方法中的this.memberValues.get(var4)
,也就是触发了LazyMap
的get
方法,通过 get()
方法获取不到 key
的时候触发 Transformer
总结
以上就是 CC1
链分析的全部内容了,最后总结一下。
利用说明:
利用 AnnotationInvocationHandler
在反序列化时会触发 Map
的 get/set
等操作,配合 TransformedMap/LazyMap
在执行 Map
对象的操作时会根据不同情况调用 Transformer
的转换方法,最后结合了 ChainedTransformer
的链式调用、InvokerTransformer
的反射执行完成了恶意调用链的构成。其中 LazyMap
的触发还用到了动态代理机制。
1 | Gadget: |
1 | 调用链: |
依赖版本
commons-collections : 3.1
TransformedMap - jdk < 8u71