实战攻防中高版本JDK反射类加载浅析
2024-7-16 17:5:49 Author: govuln.com(查看原文) 阅读量:23 收藏

Ha1ey@深蓝攻防实验室

前言

JDK9版本开始引入Java平台模块系统JPMS(Java Platform Module System),详细介绍可以看Oracle官方对于JDK9的新特性说明:https://docs.oracle.com/javase/9/whatsnew/toc.htm

关于模块之间的访问权限:

通常Java的class类访问权限分为:public、protected、private和默认的包访问权限。JDK9引入模块概念后,这些概念就要和模块的区分一下,class的这些访问权限没有失效,但是只能在模块内生效。模块和模块之间如果想要外部访问到我们的类就需要显式导出一下也就是使用expoerts

module hello.world {
    exports com.itranswarp.sample;

    requires java.base;
        requires java.xml;
}

demo如下:

import javax.management.loading.MLet;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Base64;

publicclassMain{
publicstaticvoidmain(String[] args){
try{
StringevilClassBase64="xxxx";
byte[] bytes =Base64.getDecoder().decode(evilClassBase64);
Methodmethod=ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);
            method.setAccessible(true);
Classcc=(Class) method.invoke(newMLet(newURL[0],Main.class.getClassLoader()), bytes,newInteger(0),newInteger(bytes.length));
            cc.newInstance();
}catch(Exception e){

}

}
}

将上面的demo分别在JDK11JDK21环境中测试

JDK 11

使用上述demo运行后会提示非法反射操作,并且会提示在未来的版本会完全禁用掉此类的不安全反射操作,但是不影响字节码的加载

JDK21

JDK17版本之后使用了强封装直接会ban掉非法反射,可以看报错提示java.base 模块中的 java.lang 包没有对未命名模块开放反射

Oracle官方文档给出的解释

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

这里就要去看一下JDK9之后模块化的一个指令

open, opens, opens…to 指令

在 Java 9 之前,我们可以通过反射技术来获取某个包下所有的类及其内部成员的信息,即使是 private 类型我们也能获取到,所以类信息并不是真的与外界完全隔离的。而模块系统的主要目标之一就是实现强封装,默认情况下,除非显式地导出或声明某个类为 public 类型,那么模块中的类对外部都是不可见的,模块化要求我们对外部模块应最小限度地暴露包的范围。open 相关的指令就是用来限制在运行时哪些类可以被反射技术探测到。

首先我们先看 opens 指令,语法如下:

opens package

opens 指令用于指定某个包下所有的 public 类都只能在运行时可被别的模块进行反射,并且该包下的所有的类及其乘员都可以通过反射进行访问。

opens…to 指令,语法如下:

opens package to modules

该指令用于指定某些特定的模块才能在运行时对该模块下特定包下的 public 类进行反射操作,to 后面跟逗号分隔的模块名称。

open 指令,语法如下:

open module moduleName{
}

该指令用于指定外部模块可以对该模块下所有的类在运行时进行反射操作。

也就是说JDK17+在开发的时候并没有将我们所需要的java.lang开放反射权限,导致我们无法进行反射类加载,查看JDK源码中的module-info.class定义,发现确实没有使用open指令

后面看Oracle的官方文档发现,官方预留了sun.miscsun.reflect两个包可以进行反射调用

后面看了下JDK21源码,在JDK的jdk.unsupported模块下的module-info中有声明

使用opens的指令使得两个包名下的类可以被反射。

Unsafe

关于Unsafe类的介绍可以看这篇文章:https://javaguide.cn/java/basis/unsafe.html

文中提到了它分别有defineClassdefineAnonymousClass两种方法可以加载字节码的方式。但是作者在实际JDK8、JDK11、JDK21的环境中测试中发现,JDK8同时存在两个方法,但是JDK11只剩defineAnonymousClass一种方法,甚者JDK21两种方法都被移除了。。。。

下面是三个版本的JDK的Unsafe

JDK8

JDK11

JDK21

后面去查找相关文章发现在JDK 17移除了defineAnonymousClass方法。

也就是说在 <JDK17之前都可以直接用defineAnonymousClass这个方法去进行反射类加载操作。

            Field field = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            unsafe.defineAnonymousClass(Class.class,bytes,null).newInstance();

JDK17+的字节码加载

虽然JDK模块化后官方预留了sun.miscsun.reflect两个包可以进行反射调用,但是JDK17+同时也删掉了UnsafedefineAnonymousClass方法。这就导致前面的加载方式就失效了。

后面看了@Aiwin师傅在https://xz.aliyun.com/t/14048分享通过修改当前类的module为`java.base`保持和`java.lang.ClassLoader`同module下就可以打破模块化的限制,从而可以加载字节码文件。

  private booleancheckCanSetAccessible(Class<?> caller,
                                          Class<?> declaringClass,
                                          boolean throwExceptionIfDenied){
if(caller ==MethodHandle.class){
thrownewIllegalCallerException();// should not happen
}

ModulecallerModule= caller.getModule();
ModuledeclaringModule= declaringClass.getModule();

if(callerModule == declaringModule)returntrue;
if(callerModule ==Object.class.getModule())returntrue;
if(!declaringModule.isNamed())returntrue;

Stringpn= declaringClass.getPackageName();
int modifiers;
if(thisinstanceofExecutable){
            modifiers =((Executable)this).getModifiers();
}else{
            modifiers =((Field)this).getModifiers();
}

// class is public and package is exported to caller
booleanisClassPublic=Modifier.isPublic(declaringClass.getModifiers());
if(isClassPublic && declaringModule.isExported(pn, callerModule)){
// member is public
if(Modifier.isPublic(modifiers)){
                logIfExportedForIllegalAccess(caller, declaringClass);
returntrue;
}

// member is protected-static
if(Modifier.isProtected(modifiers)
&&Modifier.isStatic(modifiers)
&& isSubclassOf(caller, declaringClass)){
                logIfExportedForIllegalAccess(caller, declaringClass);
returntrue;
}
}

// package is open to caller
if(declaringModule.isOpen(pn, callerModule)){
            logIfOpenedForIllegalAccess(caller, declaringClass);
returntrue;
}

if(throwExceptionIfDenied){
// not accessible
Stringmsg="Unable to make ";
if(thisinstanceofField)
                msg +="field ";
            msg +=this+" accessible: "+ declaringModule +" does not \"";
if(isClassPublic &&Modifier.isPublic(modifiers))
                msg +="exports";
else
                msg +="opens";
            msg +=" "+ pn +"\" to "+ callerModule;
InaccessibleObjectExceptione=newInaccessibleObjectException(msg);
if(printStackTraceWhenAccessFails()){
                e.printStackTrace(System.err);
}
throw e;
}
returnfalse;
    }

从上述的方法中可以看到主要是检查了class的moudle,Unsafe类中恰好可以修改偏移量,将我们的类的module修改成基础module就可以绕过JDK17+版本的反射限制。

        String evilClassBase64="xxxx";
byte[] bytes =Base64.getDecoder().decode(evilClassBase64);
ClassunsafeClass=Class.forName("sun.misc.Unsafe");
Fieldfield= unsafeClass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
Unsafeunsafe=(Unsafe) field.get(null);
ModulebaseModule=Object.class.getModule();
ClasscurrentClass=Main.class;
longoffset= unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
        unsafe.putObject(currentClass, offset, baseModule);
Methodmethod=ClassLoader.class.getDeclaredMethod("defineClass",byte[].class,int.class,int.class);
        method.setAccessible(true);
((Class)method.invoke(ClassLoader.getSystemClassLoader(), bytes,0, bytes.length)).newInstance();

本文首发于先知社区:https://xz.aliyun.com/t/15035


文章来源: https://govuln.com/news/url/NBMA
如有侵权请联系:admin#unsafe.sh