ClassLoader(类加载机制)

    JVM架构图:

    Java是编译型语言,我们编写的java文件需要编译成后class文件后才能够被JVM运行,学习ClassLoader之前我们先简单了解下Java类。

    示例TestHelloWorld.java:

    编译TestHelloWorld.javajavac TestHelloWorld.java

    我们可以通过JDK自带的javap命令反汇编TestHelloWorld.class文件对应的com.anbai.sec.classloader.TestHelloWorld类,以及使用Linux自带的hexdump命令查看TestHelloWorld.class文件二进制内容:

    image-20191217171821663

    JVM在执行TestHelloWorld之前会先解析class二进制内容,JVM执行的其实就是如上javap命令生成的字节码(ByteCode)。

    ClassLoader

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

    ClassLoader类有如下核心方法:

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

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

    常用的类动态加载方式:

    1. // 反射加载TestHelloWorld示例
    2. Class.forName("com.anbai.sec.classloader.TestHelloWorld");
    3. // ClassLoader加载TestHelloWorld示例
    4. this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");

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

    ClassLoader类加载流程

    理解Java类加载机制并非易事,这里我们以一个Java的HelloWorld来学习ClassLoader

    ClassLoader加载com.anbai.sec.classloader.TestHelloWorld类重要流程如下:

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

    java.lang.ClassLoader是所有的类加载器的父类,java.lang.ClassLoader有非常多的子类加载器,比如我们用于加载jar包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件。

    如果com.anbai.sec.classloader.TestHelloWorld类存在的情况下,我们可以使用如下代码即可实现调用hello方法并输出:

    但是如果com.anbai.sec.classloader.TestHelloWorld根本就不存在于我们的classpath,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestHelloWorld类,最后通过反射机制就可以调用TestHelloWorld类的hello方法了。

    TestClassLoader示例代码:

    1. package com.anbai.sec.classloader;
    2. import java.lang.reflect.Method;
    3. /**
    4. * Creator: yz
    5. * Date: 2019/12/17
    6. */
    7. public class TestClassLoader extends ClassLoader {
    8. private static String testClassName = "com.anbai.sec.classloader.TestHelloWorld";
    9. // TestHelloWorld类字节码
    10. private static byte[] testClassBytes = new byte[]{
    11. -54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
    12. 16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
    13. 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
    14. 1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
    15. 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
    16. 101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
    17. 114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
    18. 32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
    19. 115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
    20. 116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
    21. 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
    22. 0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
    23. 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
    24. 0, 0, 0, 2, 0, 12
    25. };
    26. @Override
    27. public Class<?> findClass(String name) throws ClassNotFoundException {
    28. // 只处理TestHelloWorld类
    29. if (name.equals(testClassName)) {
    30. // 调用JVM的native方法定义TestHelloWorld类
    31. return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
    32. }
    33. return super.findClass(name);
    34. }
    35. public static void main(String[] args) {
    36. // 创建自定义的类加载器
    37. try {
    38. // 使用自定义的类加载器加载TestHelloWorld类
    39. Class testClass = loader.loadClass(testClassName);
    40. // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
    41. Object testInstance = testClass.newInstance();
    42. // 反射获取hello方法
    43. Method method = testInstance.getClass().getMethod("hello");
    44. // 反射调用hello方法,等价于 String str = t.hello();
    45. String str = (String) method.invoke(testInstance);
    46. System.out.println(str);
    47. } catch (Exception e) {
    48. e.printStackTrace();
    49. }
    50. }
    51. }

    利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测,也可以用于加密重要的Java类字节码(只能算弱加密了)。

    URLClassLoader

    URLClassLoader继承了ClassLoaderURLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。

    TestURLClassLoader.java示例:

    远程的cmd.jar中就一个CMD.class文件,对应的编译之前的代码片段如下:

    1. import java.io.IOException;
    2. /**
    3. * Creator: yz
    4. * Date: 2019/12/18
    5. */
    6. public class CMD {
    7. public static Process exec(String cmd) throws IOException {
    8. return Runtime.getRuntime().exec(cmd);
    9. }
    10. }

    程序执行结果如下: