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
