java之jdk17反射机制绕过深入剖析
字数 1956 2025-08-29 22:41:10
JDK17反射机制绕过深入剖析
问题背景
在JDK17中,Java的反射机制受到了更严格的模块化限制,导致传统的反射调用方式(如defineClass方法加载恶意字节码)会遇到访问限制问题。本文深入分析JDK17反射机制的限制及绕过方法。
反射调用限制分析
当通过反射调用defineClass方法时,会在setAccessible方法处报错。深入跟踪发现关键在于checkCanSetAccessible方法的限制逻辑:
- 调用者模块与声明模块相同:如果调用者的模块(
callerModule)和声明该方法的模块(declaringModule)是同一个模块,返回true - 调用者模块是java.base:如果调用者所在的模块是
java.base,返回true - 未命名模块:如果
declaringModule是一个未命名模块(Unnamed Module),返回true - 公共类与公共成员:如果声明的类是
public,并且该类所在的包被导出给调用者模块(declaringModule.isExported(pn, callerModule)),且成员是public,则可以访问 - protected静态成员:如果成员是
protected且static,并且调用者类是声明类的子类(isSubclassOf(caller, declaringClass)),则可以访问 - 开放包:如果声明类的包通过
open关键字开放给调用者模块(declaringModule.isOpen(pn, callerModule)),则可以访问
Unsafe绕过技术
基本原理
在JDK9+的模块化机制中,sun.misc和sun.reflect包下的类可以正常反射使用。关键利用点在于:
通过Unsafe类修改调用类的module属性为java.base,使checkCanSetAccessible方法返回true,从而允许反射调用。
实现步骤
- 获取Unsafe对象:
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
- 获取java.base模块引用:
Module baseModule = Object.class.getModule();
- 修改当前类的module属性:
Class<?> currentClass = getClass();
unsafe.putObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), baseModule);
技术原理
declaringModule通过declaringClass.getModule()获取,为module java.base- 将当前类的
module属性设置为Object.class.getModule()的值(也是java.base) - Unsafe的
putObject方法直接操作JVM内存,绕过私有属性限制 - 设为同一module后,
setAccessible(true)不会报错
TemplatesImpl类的模块化限制
问题分析
JDK17的模块化机制导致无法直接使用TemplatesImpl类。尝试使用CB链时会直接报错,因为:
- 类访问限制:与之前情况不同,这里的问题是类本身就不能访问
- 反射获取实例:可以使用Unsafe反射获取
TemplatesImpl实例并赋值 - 反序列化问题:在调用
compare方法时仍会报错,因为模块化机制限制了getter方法的调用
代理类绕过尝试
通过JdkDynamicAopProxy代理类间接调用:
- 代理类调用
getOutputProperties方法(public方法) - 代理类可以自由调用
- 最终会调用到
TemplatesImpl的getOutputProperties方法
问题出现:
- 到达
defineTransletClasses方法后 - 调用
defineClass加载恶意类时失败 - 原因:恶意类继承
AbstractTranslet,而AbstractTranslet受模块化限制
继承限制
- 不继承
AbstractTranslet:无法通过判断,不会实例化恶意类 - 继承
AbstractTranslet:受模块化限制导致加载失败
JdbcRowSetImpl类分析
JdbcRowSetImpl类的getDatabaseMetaData方法可用于JNDI注入:
- 是一个
getter方法 - 问题:仅在
JdbcRowSetImpl类中存在,无法用代理类绕过
适用场景总结
对于以下情况可以使用代理类绕过:
- 方法受模块化影响
- 接口类有对应方法
- sink点不涉及模块化限制
完整绕过示例代码
// 获取Unsafe实例
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
// 获取java.base模块
Module baseModule = Object.class.getModule();
// 修改当前类的module属性
Class<?> currentClass = getClass();
unsafe.putObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), baseModule);
// 现在可以进行反射调用
Class<?> clazz = Class.forName("sun.reflect.misc.Trampoline");
Method method = clazz.getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
method.setAccessible(true);
结论
JDK17的模块化机制增加了反射使用的限制,但通过Unsafe类直接操作内存修改模块属性,可以在特定条件下绕过这些限制。然而,对于某些类(如TemplatesImpl)的核心功能仍受到严格限制,需要寻找其他利用链或方法。