freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

实战网络攻防中的高版本JDK反射类加载浅析
2024-09-29 15:20:50
所属地 北京

前言

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;

public class Main {
    public static void main(String[] args)  {
        try {
            String evilClassBase64 = "xxxx";
            byte[] bytes = Base64.getDecoder().decode(evilClassBase64);
            Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
            method.setAccessible(true);
            Class cc = (Class) method.invoke(new MLet(new URL[0], Main.class.getClassLoader()), bytes, new Integer(0), new Integer(bytes.length));
            cc.newInstance();
        }catch (Exception e){

        }

    }
}

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

JDK 11

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

image-20240713113628934.png

image-20240712180200779.png

JDK21

image-20240713112635059.png

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指令

image-20240713114211261.png

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

image-20240713124958677.png

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

image-20240713125318846.png

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

Unsafe

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

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

下面是三个版本的JDK的Unsafe

JDK8

image-20240713130839765.png

JDK11

image-20240713130925459.png

JDK21

image-20240713131016878.png

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

image-20240713131859556.png

也就是说在 <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 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)) {
                logIfExportedForIllegalAccess(caller, declaringClass);
                return true;
            }

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

        // package is open to caller
        if (declaringModule.isOpen(pn, callerModule)) {
            logIfOpenedForIllegalAccess(caller, declaringClass);
            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;
    }

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

String evilClassBase64 = "xxxx";
        byte[] bytes = Base64.getDecoder().decode(evilClassBase64);
        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 offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
        unsafe.putObject(currentClass, offset, baseModule);
        Method method = ClassLoader.class.getDeclaredMethod("defineClass",byte[].class, int.class, int.class);
        method.setAccessible(true);
        ((Class)method.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length)).newInstance();

image-20240714223046919.png

# 漏洞 # 黑客 # 网络安全 # web安全 # 漏洞分析
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录