ClassLoader
JAVA类
Java语言无法被操作系统直接识别,需要经过JVM将.java
文件编译为.class
文件(编译),后续JVM在执行程序时便首先解析class
文件的二进制内容,执行.class
文件的字节码(解释)
ClassLoader加载流程
类加载器
- Bootstrap ClassLoader (引导类加载器),最顶层加载器,主要加载核心类库,由 C++ 实现, 负责加载
%JAVA_HOME%lib
目录中的 java 核心类库, 路径也可由-Xbootclasspath
参数指定 - Extention ClassLoader(扩展类加载器),由
sun.misc.Launcher$ExtClassLoader
实现, 负责加载%JAVA_HOMElibext
目录中的 java 扩展库, 路径也可由-Djava.ext.dirs
参数指定 - Appclass Loader(系统类加载器,SystemAppClass),默认加载器,在不指定的情况下,默认使用Appclass Loader由
sun.misc.Launcher$AppClassLoader
实现, 负责加载当前 classpath 下的 class 文件, 路径也可由-Djava.class.path
参数指定 - UserDefineClassLoader: 为开发者自行编写, 通过继承
java.lang.ClassLoader
并重写相关方法来自定义 ClassLoader
ClassLoader类具有的核心方法有:
loadclass
:加载指定的Java类findclass
:查找指定的java类findLoadedClass
:查找JVM已经加载过的类defineClass
:定义一个Java类resolveClass
:链接指定的Java类
加载器加载顺序为:
- Bootstrap ClassLoader
- Extention ClassLoader
- Appclass Loader
加载顺序流程(双亲委派机制)如下
ClassLoader
加载com.anbai.sec.classloader.TestHelloWorld
类loadClass
重要流程如下:
ClassLoader
会调用public Class<?> loadClass(String name)
方法加载com.anbai.sec.classloader.TestHelloWorld
类。- 调用
findLoadedClass
方法检查TestHelloWorld
类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。- 如果创建当前
ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
)就使用父类加载器加载TestHelloWorld
类,否则使用JVM的Bootstrap ClassLoader
加载。- 如果上一步无法加载
TestHelloWorld
类,那么调用自身的findClass
方法尝试加载TestHelloWorld
类。- 如果当前的
ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的com.anbai.sec.classloader.TestHelloWorld
类名找到了对应的类字节码,那么应该调用defineClass
方法去JVM中注册该类。- 如果调用loadClass的时候传入的
resolve
参数为true,那么还需要调用resolveClass
方法链接类,默认为false。- 返回一个被JVM加载后的
java.lang.Class
类对象。
其中,三四步属于双亲委托机制,优先使用父类加载器加载
不同的ClassLoader会有父子关系,而非继承关系,本质是Java.lang.ClassLoader
内部定义了指向父加载器的常量parent,可以通过方法getParent()
方法获取父加载器
ClassLoader cl = Test.class.getClassLoader();
System.out.println("cl's parent is " + cl.getParent().toString());
eg:AppClass Loader的父类加载器为Extention ClassLoader
但是,再次获取Extention ClassLoader的父类加载器会发生一个空指针报错
(参考x1r0z大佬博客):Extention ClassLoader的父类加载器BoostrapClassLoader 是由 C++ 实现, 无法在 Java 中获取对应的引用, 所以显示 null
双亲委派机制
当前 ClassLoader 在加载 class 时, 会将被加载的 class 委托给它的父加载器加载, 以此类推直到最顶层的 BootstrapClassLoader, 如果 BootstrapClassLoader 无法加载这个 class, 则会抛出异常, 然后被子加载器捕获, 并由子加载器继续尝试加载, 如果仍然无法加载, 就一层一层往下直到最开始的 ClassLoader, 如果这个 ClassLoader 也无法加载对应 class, 最终则会抛出 java.lang.ClassNotFoundException
异常
双亲委派的机制的优势在于:
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
打破双亲委派机制
实际项目例如:JDBC、JNDI、Tomcat等都打破了双亲委派机制
仅仅要靠 JVM 中的根加载器是无法加载进入 JVM 运行的,所以这时候我们就要破坏双亲委派机制,实现框架内的类由框架内的加载器加载。
自定义一个 ClassLoader, 并且重写一个 loadclass 即可打破双亲委派机制
自定义ClassLoader
Hello类:
public class Hello {
public Hello(){
try{
Runtime.getRuntime().exec("calc.exe");
}catch (Exception e){
e.printStackTrace();
}
}
}
自定义ClassLoader 加载Hello类
FileClassLoader:
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileClassLoader extends ClassLoader{
protected String basePath;
public FileClassLoader(String basePath){
super();
this.basePath=basePath;
}
@Override
protected Class<?> findClass(String name){
byte[] arr;
try{
Path path = Paths.get(this.basePath, name+".class");
arr = Files.readAllBytes(path);
return defineClass(name,arr,0,arr.length);
}catch (IOException e){
e.printStackTrace();
}
return null;
}
public static void main(String[] args)throws Exception{
ClassLoader classLoader = new FileClassLoader("D:\");
Class clazz = classLoader.loadClass("Hello");
clazz.newInstance();
}
}
URLClassLoader
继承自java.lang.ClassLoader
,提供了加载远程jar包的能力,可以在漏洞利用时利用该方法来加载远程的jar实现远程的类方法代用
将上一步写的Hello.java
文件编译出的Hello.class
文件放在网站根目录下
编写远程加载payload:
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
public class URLLoader {
public static void main(String[] args)throws Exception{
URL url = new URL("http://localhost");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class clazz = loader.loadClass("Hello");
clazz.newInstance();
}
}
类加载隔离
创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两者必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。
跨类加载器加载
RASP和IAST经常会用到跨类加载器加载类的情况,因为RASP/IAST会在任意可能存在安全风险的类中插入检测代码,因此必须得保证RASP/IAST的类能够被插入的类所使用的类加载正确加载,否则就会出现ClassNotFoundException,除此之外,跨类加载器调用类方法时需要特别注意一个基本原则:ClassLoader A和ClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class A和ClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射
。
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。