参照https://javasec.org/以及p神的Java安全漫谈的路线进行学习,类似读书笔记那种吧。之前都是ctf遇到Java的题才学一点,像是反序列化这种,没系统化的学过Java Web安全,这次从头来好好学一遍。
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学习的基础。
Java是一个依赖于JVM
(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件
,Java类初始化的时候使用类加载器创建。
加载器分为两种:由 Java 虚拟机提供的引导类加载器,以及用户定义的类加载器。 每个用户定义的类加载器都是一个ClassLoader
抽象类的子类的实例.
Java类初始化的时候会调用java.lang.ClassLoader
加载类字节码,ClassLoader
会调用JVM的native方法(defineClass0/1/2
)来定义一个java.lang.Class
实例(创建一个对象)。
JVM架构图:
一切的Java类都必须经过JVM加载后才能运行,而ClassLoader
的主要作用就是Java类文件.class 的加载。将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。
.class文件结构示例
JVM类加载器内置了三个重要的类加载器,三者形成层次结构,从上到下依次为:
Bootstrap ClassLoader(引导类加载器,内嵌在java虚拟机中由C++编写)
主要加载核心类库,ClassLoader就是是由它来加载的,它并不是Java类,而其它加载器则都是Java类。Extension ClassLoader(扩展类加载器)
,负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中。Java9之后更名为platform classloaderApp ClassLoader(系统类加载器)
,AppClassLoader
会加载 Classpath 环境变量里定义的路径中的 jar 包和目录,是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类,ClassLoader.getSystemClassLoader()
返回的系统类加载器也是AppClassLoader
。jdk还内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程/本地类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。
这三个类加载器实例的父子关系如上图,不过虽然ExtClassLoader是AppClassLoader父加载器,但却是由Bootstrap加载的AppClassLoader
如同上图当中的例子所示,某些时候我们获取一个类的类加载器时,可能会返回一个null
值,如:java.io.File.class.getClassLoader()
将返回一个null
对象,因为java.io.File
类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)
加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader
时候都会返回null
。
还需注意的是上面所提到的父子加载器并不是类继承上的父子关系,是类加载器实例之间的关系。类继承关系如下图所示:
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类)findClass
(查找指定的Java类)findLoadedClass
(查找JVM已经加载过的类)defineClass
(定义一个Java类)resolveClass
(链接指定的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()
则会报错。
如上图所示,必须等到Class.forName
执行才完成对Tester类的初始化。
先了解一下JVM的三种主要类加载机制:
缓存机制:
保证所有加载过的Class都会被缓存。即当需要某个类时会先从缓存区中搜寻该Class,若没有则进行加载。(修改Class后,必须重启JVM)
JVM判断两个类对象相同的两个条件:
父类委托 /双亲委派:
某个特定的类加载器在接到加载类的请求时,首先判断这个class是否以及加载成功,如果没有则将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
委托机制的意义 — 防止内存中出现多份同样的字节码。
全盘负责:
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的Class也将由其负责载入,除非显式指定另一加载器。
ClassLoader
加载Tester
类重要流程如下:
ClassLoader
会调用public Class> loadClass(String name)
方法加载Tester
类。findLoadedClass
方法检查Tester
类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
)就使用父类加载器加载Tester
类,否则使用JVM的Bootstrap ClassLoader
加载。Tester
类,那么调用自身的findClass
方法尝试加载Tester
类。ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的Tester
类名找到了对应的类字节码,那么应该调用defineClass
方法去JVM中注册该类。resolve
参数为true,那么还需要调用resolveClass
方法链接类,默认为false。java.lang.Class
类对象。创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两则必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。
BCEL ClassLoader
BCEL(
Apache Commons BCEL™
)是一个用于分析、创建和操纵Java类文件的工具库,Oracle
JDK引用了BCEL库,不过修改了原包名org.apache.bcel.util.ClassLoader
为com.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类名解码:
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
是JVM中一个非常重要的组成部分,ClassLoader
可以为我们加载任意的java类(配合反射机制实现,使得Java这一静态语言具有一定的动态性),通过自定义ClassLoader
更能够实现自定义类加载行为。