Understanding Java Class Loaders and Custom Implementation

Class Loading and Compilation

Class loaders serve the purpose of loading Java classes (specifically .class bytecode files that have been compiled from .java source files) into JVM memory for execution.

Compiling Java files

Package: package com.melody.sec.classloader;, Class name: DefineClassDemo

  1. Compile Java file
    javac com/melody/sec/classloader/DefineClassDemo.java
    
  2. Execute class file
    java com.melody.sec.classloader.DefineClassDemo
    
  3. Disassemble class file
    javap -c -l -p com.melody.sec.classloader.DefineClassDemo
    
  4. View bytecode content
    hexdump -C com/melody/sec/classloader/DefineClassDemo.class
    

Note:

When running Java applications, class concepts apply, so class file execution uses fully qualified class names. However, when compiling code or examining file contents, relative paths are used. «Summary: Use file paths when traeting as files, use class names when using as classes».

Class Loader Hierarchy

There are four primary class loaders (Bootstrap ClassLoader, Extension ClassLoader, Application ClassLoader). The parent delegation mechanism prevents attackers from modifying standard library classes by ensuring core classes are loaded through trusted parent loaders first.

Parent Delegation Mechanism: This design prevents malicious modifications to standard library classes. The mechanism ensures that when loading a class, the parent loader attempts to load it first before delegating to child loaders.

«Summary: For any given class name (the binary name of a class), always attempt loading through parent class loader first, then fall back to child loaders if unsuccessful».

Example binary class names:

"java.lang.String"
"javax.swing.JSpinner$DefaultEditor"
"java.security.KeyStore$Builder$FileBuilder$1"
"java.net.URLClassLoader$3$1"

Class Loading Process:

Class Name → File Path → Locate Class File → Read Bytecode → Load into JVM

After understanding the loading process, several questions emerge:

  1. How do we obtain class loaders and identify their types?
  2. How do class loaders load specific classes?
  3. Do all classes have associated class loaders?

Let's address these questions systematically.

Retrieving Class Loaders

Every class object contains a reference to its defining class loader. Therefore, retrieving class loaders involves accessing them from class objects.

  1. Get current class loader
    this.getClass().getClassLoader(); // sun.misc.Launcher$AppClassLoader@18b4aac2
    
  2. Get class loader for external packages
    ZipPath.class.getClassLoader(); // sun.misc.Launcher$ExtClassLoader@28d93b30
    
  3. Get class loader for JVM-initial classes (Bootstrap ClassLoader)
    File.class.getClassLoader();        // null
    

Do All Classes Have Class Loaders?

After learning how to retrieve class loaders, we can address question three: Do all classes have associated class loaders?

The official documentation identifies two special cases:

  1. Array classes are not created by class loaders but by the Java runtime automatically. However, the class loader for an array class matches that of its element type.
    public void examineArrayLoaders(){
        // Do all classes have corresponding class loaders?
        // Array class objects aren't created by class loaders but automatically by Java runtime.
        // The class loader for an array class matches its element type's loader.
        DefineClassDemo[] instances = new DefineClassDemo[3];
        ClassLoader loader = instances.getClass().getClassLoader();
        System.out.println(loader);    // sun.misc.Launcher$AppClassLoader@18b4aac2
    }
    
  2. If the element type is primitive, the array class has no class loader
    // If element type is primitive, array class has no class loader
    char[] chars = "hello".toCharArray();
    System.out.println(chars.getClass().getClassLoader());  // null
    

How Class Loaders Load Classes

Where do classes load from?

  1. From the file system (such as classpath defined locations)
  2. From remote servers

How do classes get loaded?

  1. Implicit loading: Creating objects via new causes JVM to load corresponding classes into memory

    «Like a factory receiving an order, workers search for appropriate molds and produce items for customers»

    new ClassInstance or ClassName.methodName
    
  2. Explicit loading: Using Class.forName allows developers direct control over loading

    «Like factory workers using available molds and materials to create desired items»

    // Using Class.forName loading example
    TestHelloWorld obj = (TestHelloWorld) Class.forName("com.melody.sec.classloader.TestHelloWorld").newInstance();
    obj.sayHello();
    
    // Using ClassLoader loading example
    TestHelloWorld obj = (TestHelloWorld)DefineClassDemo.class.getClassLoader().loadClass("com.melody.sec.classloader.TestHelloWorld").newInstance();
    obj.sayHello();
    

Now that we understand how to use default class loaders to load classes, we can leverage inheritance to create our own custom ClassLoader.

Custom Class Loaders

As mentioned earlier, class loaders can load bytecode from various sources beyond the file system, including network sources. A notable example is URLClassLoader. Let's start with a simple custom class loader implementation, then explore URLClassLoader.

  1. Create a malicious class with a method returning malicious strings
    package com.melody.sec.classloader;
    
    /**
     * @author Me1ody
     * @version 1.0
     */
    public class MaliciousClass {
        public String executeAttack(){
            return "MaliciousClass has been injected!";
        }
    }
    
  2. Compile this class to bytecode
  3. Read bytecode file and convert to byte[] array
    public static String maliciousFile = "absolute/path/com/melody/sec/classloader/MaliciousClass.class";
    public static void main(String[] args) {
        try {
            FileInputStream input = new FileInputStream(maliciousFile);
            ByteArrayOutputStream output = new ByteArrayOutputStream();
    
            int size;
            byte[] buffer = new byte[1024];
            while ((size = input.read(buffer)) != -1) {
                output.write(buffer, 0, size);
            }
    
            byte[] bytecode = output.toByteArray();
            for (byte b : bytecode) {
                System.out.print(b + ", ");
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    

Our custom class loader approach assumes the target class (MaliciousClass) doesn't exist in the classpath. Normally, attempting to load it would result in a class not found error. Our custom loader will successfully load it when requested.

To implement this custom loader, we need to accomplish two tasks:

  1. Create a new class extending java.lang.ClassLoader
  2. Override the findClass method of ClassLoader

Implementation code:

package com.melody.sec.classloader;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author Me1ody
 * @version 1.0
 * Custom Class Loader
 * 1. Extend ClassLoader class
 * 2. Override findClass method
 */
public class CustomClassLoader extends ClassLoader{
    private static String targetClass = "com.melody.sec.classloader.MaliciousClass";

    private static byte[] bytecodeData = new byte[] {
            -54, -2, -70, -66, 0, 0, 0, 52, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0, 16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 8, 115, 97, 121, 72, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 14, 69, 118, 105, 108, 67, 108, 97, 115, 115, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 28, 69, 118, 105, 108, 67, 108, 97, 115, 115, 32, 104, 97, 115, 32, 98, 101, 101, 110, 32, 105, 110, 106, 101, 99, 116, 101, 100, 33, 1, 0, 36, 99, 111, 109, 47, 109, 101, 108, 111, 100, 121, 47, 115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 69, 118, 105, 108, 67, 108, 97, 115, 115, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 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, 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, 0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 9, 0, 1, 0, 11, 0, 0, 0, 2, 0, 12
    };

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (name.equals(targetClass)) {
            return defineClass(targetClass, bytecodeData, 0, bytecodeData.length);
        }

        return super.findClass(name);
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        CustomClassLoader customLoader = new CustomClassLoader();

        Class<?> loadedClass = customLoader.loadClass(targetClass);
        Object instance = loadedClass.newInstance();
        Method attackMethod = instance.getClass().getMethod("executeAttack");
        String result = (String) attackMethod.invoke(instance);
        System.out.println(result);
    }
}

«Custom class loaders enable loading and executing compiled classes in webshells, potentially bypassing RASP detection by invoking native methods from custom bytecode, or encrypting important Java class bytecode (though this provides weak encryption).» (Specific applications will be detailed later)

Tags: java class-loader custom-classloader JVM Bytecode

Posted on Wed, 10 Jun 2026 18:24:08 +0000 by noobcody