Javaweb安全学习——Java类加载机制

前言

参照https://javasec.org/以及p神的Java安全漫谈的路线进行学习,类似读书笔记那种吧。之前都是ctf遇到Java的题才学一点,像是反序列化这种,没系统化的学过Java Web安全,这次从头来好好学一遍。

Java平台版本

Java平台共分为三个主要版本Java SE(Java Platform, Standard Edition-Java平台标准版)、Java EE(Java Platform Enterprise Edition-Java平台企业版)、和Java ME(Java Platform, Micro Edition-Java平台微型版)。Java SE是JDK自带的标准API,是Java学习的基础。

ClassLoader(类加载机制)

Java是一个依赖于JVM(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件,Java类初始化的时候使用类加载器创建。

加载器分为两种:由 Java 虚拟机提供的引导类加载器,以及用户定义的类加载器。 每个用户定义的类加载器都是一个ClassLoader抽象类的子类的实例.

Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVM的native方法(defineClass0/1/2)来定义一个java.lang.Class实例(创建一个对象)。

JVM架构图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VBbbVw9z-1648785239285)(.\images\image-20220327174303106.png)]

ClassLoader

一切的Java类都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件.class 的加载。将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RRYDZGH8-1648785239286)(.\images\image-20220327182300725.png)]

.class文件结构示例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XEniZqiR-1648785239287)(.\images\image-20220327182755049.png)]

JVM类加载器内置了三个重要的类加载器,三者形成层次结构,从上到下依次为:

  1. Bootstrap ClassLoader(引导类加载器,内嵌在java虚拟机中由C++编写)主要加载核心类库,ClassLoader就是是由它来加载的,它并不是Java类,而其它加载器则都是Java类。
  2. Extension ClassLoader(扩展类加载器),负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中。Java9之后更名为platform classloader
  3. App ClassLoader(系统类加载器)AppClassLoader会加载 Classpath 环境变量里定义的路径中的 jar 包和目录,是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader加载类,ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader

jdk还内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程/本地类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

image-20220327214341196

这三个类加载器实例的父子关系如上图,不过虽然ExtClassLoader是AppClassLoader父加载器,但却是由Bootstrap加载的AppClassLoader

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vcw820Fv-1648785239287)(.\images\image-20220327220054896.png)]

如同上图当中的例子所示,某些时候我们获取一个类的类加载器时,可能会返回一个null值,如:java.io.File.class.getClassLoader()将返回一个null对象,因为java.io.File类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null

还需注意的是上面所提到的父子加载器并不是类继承上的父子关系,是类加载器实例之间的关系。类继承关系如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97bNjMJi-1648785239288)(.\images\image-20220327222432186.png)]

ClassLoader类有如下核心方法:

  1. loadClass(加载指定的Java类)
  2. findClass(查找指定的Java类)
  3. findLoadedClass(查找JVM已经加载过的类)
  4. defineClass(定义一个Java类)
  5. resolveClass(链接指定的Java类)

Java类动态加载方式

Java类加载方式分为显式隐式,显式即我们通常使用Java反射或者ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。

常用的类动态加载方式:

// 反射加载TestHelloWorld示例
Class.forName("HelloTest");

// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("HelloTest");

Class.forName("类名")默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法。Class.forName() 方法可以获取原生类型的 Class,而ClassLoader.loadClass()则会报错。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ei65JxUq-1648785239288)(.\images\image-20220327212241438.png)]

如上图所示,必须等到Class.forName执行才完成对Tester类的初始化。

ClassLoader类加载流程

先了解一下JVM的三种主要类加载机制:

缓存机制:

保证所有加载过的Class都会被缓存。即当需要某个类时会先从缓存区中搜寻该Class,若没有则进行加载。(修改Class后,必须重启JVM)

JVM判断两个类对象相同的两个条件:

  1. 类的完整名必须一致
  2. 加载这个类的ClassLoader实例必须为同一个

父类委托 /双亲委派:

某个特定的类加载器在接到加载类的请求时,首先判断这个class是否以及加载成功,如果没有则将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

委托机制的意义 — 防止内存中出现多份同样的字节码。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iG70lsQi-1648785239289)(images/image-20220328001202901.png)]

全盘负责:

当一个类加载器负责加载某个Class时,该Class所依赖的和引用的Class也将由其负责载入,除非显式指定另一加载器。

ClassLoader加载Tester类重要流程如下:

  1. ClassLoader会调用public Class<?> loadClass(String name)方法加载Tester类。
  2. 调用findLoadedClass方法检查Tester类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。
  3. 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载Tester类,否则使用JVM的Bootstrap ClassLoader加载。
  4. 如果上一步无法加载Tester类,那么调用自身的findClass方法尝试加载Tester类。
  5. 如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的Tester类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。
  6. 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。
  7. 返回一个被JVM加载后的java.lang.Class类对象。

类加载隔离

创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两则必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pfrv2BDd-1648785239289)(images/202110251829223.png)]

BCEL ClassLoader

BCELApache Commons BCEL™)是一个用于分析、创建和操纵Java类文件的工具库,Oracle
JDK引用了BCEL库,不过修改了原包名org.apache.bcel.util.ClassLoadercom.sun.org.apache.bcel.internal.util.ClassLoader,BCEL的类加载器在解析类名时会对ClassName中有$$BCEL$$标识的类做特殊处理,该特性经常被用于编写各类攻击Payload。

BCEL攻击原理

当BCEL的com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass加载一个类名中带有$$BCEL$$的类时会截取出$$BCEL$$后面的字符串,然后使用com.sun.org.apache.bcel.internal.classfile.Utility#decode将字符串解析成类字节码(带有攻击代码的恶意类),最后会调用defineClass注册解码后的类,一旦该类被加载就会触发类中的恶意代码,正是因为BCEL有了这个特性,才得以被广泛的应用于各类攻击Payload中。

示例 - BCEL类名解码:

image-20211021104833683

BCEL编解码

BCEL编码:

byte[]{
     
     类字节码byte数组}];

// BCEL编码类字节码 String className = "$$BCEL$$" +
com.sun.org.apache.bcel.internal.classfile.Utility.encode(CLASS_BYTES,
true); ```

编码后的类名:`$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85S$dbn$d......`,BCEL会对类字节码进行编码,

**BCEL解码:**

```java int    index    = className.indexOf("$$BCEL$$"); String
realName = className.substring(index + 8);

// BCEL解码类字节码 byte[] bytes =
com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName,
true); ```

如果被加载的类名中包含了`$$BCEL$$`关键字,BCEL就会使用特殊的方式进行解码并加载解码之后的类。

## ClassLoader总结

`ClassLoader`是JVM中一个非常重要的组成部分,`ClassLoader`可以为我们加载任意的java类(配合反射机制实现,使得Java这一静态语言具有一定的动态性),通过自定义`ClassLoader`更能够实现自定义类加载行为。

码类字节码 byte[] bytes =
com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName,
true); ```

如果被加载的类名中包含了`$$BCEL$$`关键字,BCEL就会使用特殊的方式进行解码并加载解码之后的类。

ClassLoader总结

ClassLoader是JVM中一个非常重要的组成部分,ClassLoader可以为我们加载任意的java类(配合反射机制实现,使得Java这一静态语言具有一定的动态性),通过自定义ClassLoader更能够实现自定义类加载行为。

猜你喜欢

转载自blog.csdn.net/weixin_43610673/article/details/123894319
今日推荐