类加载机制
Java是一门跨平台语言,其跨平台的能力依赖于JVM,Java的.java源代码文件都必须经过编译为.class字节码文件后,由JVM加载才能运行,因为JVM在不同平台上都有实现,也就赋予了Java跨平台运行的特性。
CLassLoader的实现
ClassLoader是Java中的一个类,负责动态加载类文件(.class),Java提供三个重要的ClassLoader实现:
- Bootstrap ClassLoader(引导类加载器):加载Java的核心类库,通常有底层C++实现,可以直接访问JVM。
- Extension ClassLoader(拓展类加载器):加载拓展类库,通常位于
JAVA_HOME/lib/ext
。 - App ClassLoader(应用类加载器):加载应用程序的类和资源,是默认的类加载器。
Java中的类加载器遵循双亲委派模型(Parent Delegation Model),即当一个类加载器加载类是,会首先委托给父加载器,如果父加载器中找不到目标类,再由当前加载器尝试加载。每个类加载器都有一个父类加载器,但这里的父子关系不是通过继承实现的,通过“委托”实现的,即双亲委派模型。
这个委托是层层递归的,即最终会被最顶层的类加载器先加载,上面的类加载器的父子关系从上即Bootstrap==>Extension==>App,也就是说当App ClassLoader尝试加载类时,会委托给Extension ClassLoader,并最终委托至Bootstrap ClassLoader,由Bootstrap ClassLoader首先尝试加载。这样做的好处是可以防止类的重复加载,也能保证核心类库的安全性。
比如,Java核心中有一个String类(java.lang.String
),我们又写了一个恶意的java.lang.String
类来尝试覆盖,因为双亲委派模型的存在,加载恶意String类时,会被层层委派至Bootstrap ClassLoader,因为Bootstrap ClassLoader已经加载了核心类库中的java.lang.String
,则不会加载自定义的java.lang.String
。(不过JVM 也明确禁止了用户代码定义核心包名)
ClassLoader的核心方法如下:
loadClass
(加载指定的Java类):首先使用findLoadedClass方法来检查待加载的类是否被加载过,若未被加载,则调用父加载器的loadClass方法。父加载器加载失败则通过findClass来加载该类。findClass
(查找指定的Java类):是一个抽象方法,由子类具体实现。用于根据类名加载字节码并返回一个Class
对象。loadClass
方法最终会调用findClass
来加载类。findLoadedClass
(查找JVM已加载过的类):用于判断指定的类是否已经被当前的ClassLoader
加载。如果该类已经加载,则返回已加载的Class
对象;如果没有加载,则返回null
。defineClass
(定义一个Java类):用于将字节数组定义为类。通常是ClassLoader
内部使用此方法将加载的字节码转换为Class
对象。resolveClass
(链接指定的Java类):即使Class
对象可用。
自定义类加载器
新建一个自定义类加载器的步骤如下:
- 继承ClassLoader类
- 重写findClass()方法
- 实现加载类的逻辑
例如这里实现了一个自定义的类加载器,并重写findClass方法当加载类时,若匹配到的指定的类名,则通过直接提供的类的字节码来定义该类。org.example.Test类如下,是IDEA创建新项目时自动生成的:
|
|
使用javac将其编译为Test.class,再用十六进制查看器获取其字节,这里我使用的imhex,可以通过右键copy直接copy为java数组。
|
|
写一个自定义的类加载器如下:
|
|
当然也不必拘泥于此,通过该方法可以实现:从本地某路径加载类、请求网络实现类的远程加载。例如java.net
中提供了 URLClassLoader,提供了加载远程资源的能力。可以加载本地class文件或者网络的class文件。或者匹配某类-方法进行替换当做后门。
安全领域,往往使用该机制实现自定义恶意的类加载器来加载webshell,或者自定以类字节码的native方法绕过RASP检测。
Java反射机制
Java的反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于属性能够动态获取并修改其值,对于方法,能够动态调用;这种动态获取信息以及动态调用对象方法的功能,被称为Java的反射机制。
Java反射操作的是java.lang.Class
对象,获取一个类的Class对象一般有如下的方法:
类名.class
,直接获取一个已加载的类的java.lang.Class
,如org.example.Test.class
Class.forName("org.example.Test")
实例化对象.getClass()
下面的例子演示了使用如上三种方法获取类的Class对象,并使用getName来打印其类名。
反射获取成员变量
获取成员变量的方法位于java.lang.reflect.Field包中
public Field[] getFields()
,获取所有public修饰的成员变量public Field[] getDeclaredFields()
,获取所有的成员变量,不考虑修饰符public Field getField(String name)
,获取指定名称的public修饰的成员变量public Field getDeclaredField(String name)
,获取指定的成员变量,不考虑修饰符
对于Field,常用的方法有:
- getName(),获取名称
- getType(),获取类型
- get(Object obj),获取字段的值
- getModifiers(),获取字段的修饰符
- set(Object obj, Object value),设置字段的值
- setAccessible(boolean flag),设置字段的可访问性(允许访问私有字段)
|
|
运行结果:
反射获取方法
获取方法的方法位于java.lang.reflect.Method包中
public Method[] getMethods()
,获取类的所有public修饰的方法public Method[] getDeclaredMethod()
,获取类所有的方法public Method getMethod(String name, Class<?>... parameterTypes)
,获取该类声明的所有public方法;前一个参数为方法名,后面的参数列表是参数类型(都要加上.class后缀,在上一节的“自定义类加载器”部分有提到)public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
,获取该类的所有方法
对于Method,常用的方法有:
- getName(),获取方法名称
- getReturnType(),获取方法的返回类型
- getParameterTypes(),获取方法的参数类型
- public int getModifiers(),获取方法的修饰符
- invoke(Object obj, Object… args),用于调用方法
- setAccessible(boolean flag),设置访问权限,允许调用私有方法
也可以直接打印Method的实例,来获取其完整签名,其toString方法如下:
|
|
运行结果:
反射获取构造函数
获取构造函数Constructor的方法位于java.lang.reflect.Constructor包中
public Constructor<?>[] getConstructors()
,获取所有公共的构造方法public Constructor<?>[] getDeclaredConstructors()
,获取所有构造方法public Constructor<T> getConstructor(Class<?>... parameterTypes)
,获取指定的公共构造方法public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
,获取指定的构造方法
对于Constructor,常用的方法有:
- 获取名称、获取参数类型、获取修饰符,以及toString方法可以一揽子打印出来,与Method的类似不赘述了
- public T newInstance(Object … initargs),调用构造函数创建实例
- public void setAccessible(boolean flag),对私有构造函数设置访问权限
|
|
运行结果:
既然反射可以获取类中的所有方法,在有实例的情况下可以调用某函数;反射又可以获取构造函数同时新建实例,等于如果某个类中存在危险的函数,攻击者通过反射就可以直接调用了。下面尝试访问java.lang.Runtime.exec()
。
尝试通过反射执行命令
首先获取了java.lang.Runtime
中的所有构造函数,结果发现只有一个:private java.lang.Runtime()
,通过setAccessible(true)
获取其权限。获取其中的所有方法,发现exec都是public的,且有6个重载。接下来就容易了,只需要设置该私有构造函数的访问权限,接着通过getMethod("exec", String.class)
获取exec函数的一个重载,调用invoke
并传入构造函数.newInstance()
作为实例即可。
理论很美好,but失败了,报错如下:
|
|
查了下,这个错误是因为模块化系统(Java Platform Module System, JPMS)的限制导致的,从Java 9开始引入了JPMS(我这里使用的是java23),默认情况下对一些包的反射访问做了限制,java.lang.Runtime
所在的模块java.base
不提供对未命名模块(即我写的这个代码)的访问权限,因此没法直接通过反射访问Runtime的私有构造函数。
但是也有解决方案,不过对于恶意利用来说已经失去了意义:添加JVM参数,--add-opens java.base/java.lang=ALL-UNNAMED
,如下: