# Introduction

又是回过头来填坑的一篇,刚开始开 java 这个坑的时候就想着把这一块的内容记录一下。后来似乎因为一些事情忘记了,主要也是前面留下的坑太多导致的。这里我也是参考着大师傅的文章学习一下这块内容,本篇中所涉及的代码都在 https://github.com/clown-q/Clown_java 中

参考(照搬照抄)文章:[ClassLoader・攻击 Java Web 应用 -Java Web 安全] (javasec.org)

# ClassLoader

# 是什么?

首先就是这样一个问题,什么是 ClassLoader?我们编写的 java 文件都是以.java 为后缀的文件,编译器会将我们编写的 java 文件编译为.class 后缀结尾的文件

image-20231029134825196

上图中就是在 idea 中我们编写的 java 文件和经过编译器编译后的 class 文件所在位置和对应的关系。而我们编译好的 class 文件想要运行还需要一个小玩意(JVM),对于 jvm 相关的内容这里也不在这里赘述,可以翻一下我之前填的坑分类:JavaJVM | Clown の Blog = (xcu.icu)。一个 java 类想要运行必须要经过 JVM 的加载后才能正常的运行,而 Classloader 的作用就是将 class 文件加载到 JVM 中。

image-20231029135931117

Classloader 也不是什么很神秘的东西,在上图中也可以看到它实际上就是一个抽象类。其中的方法很多,但是我们主要需要关注的只有下面 4 个方法

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

# 类加载器

说到类加载不得不提的就是类加载器,主要有在 JVM 中最顶层的 Bootstrap Classloader(引导类加载器)、Extension Classloader(扩展类加载器)、App Classloader(系统类加载器)、User Classloader(用户自定义加载器)这四种类加载器。他们是以双亲委派这种机制在工作

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查类是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 父类不是 BootstrapClassloader 的时候会调用其父类进行加载
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果找不到类,则抛出 ClassNotFoundException
                    // 从非空父类加载器
                }
                if (c == null) {
                    // 如果仍未找到,则按顺序调用 findClass
                    long t1 = System.nanoTime();
                    c = findClass(name);// 这里如果没有重写这个 findClass 方法会直接抛出一个异常
                    // 这是定义类装入器;记录统计数据
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

image-20231029175922589

上面这段代码实际上已经注释的很明白了,这里放一张我自己做的图来加深一下理解

image-20231029174251222

值得注意的是。Classloader 的类加载器也是 AppClassLoader

# 自定义 ClassLoader

其实这里也没有那么高大上,实际上它的本质就是方法重写。Classloader 是所有类加载器的父类,我们可以通过重写其中的方法来加载我们的类。我热衷于弹一个计算器,绝对不是因为我懒得写新的。

package ClassLoaderStudy;
/**
 * @BelongsProject: study_java
 * @BelongsPackage: ClassLoaderStudy
 * @Author: Clown
 * @CreateTime: 2023-10-29  18:21
 */
public class UserClassloaderTest extends ClassLoader{
    public String ClassName = "B";
    public byte[] bytes = new byte[]{-54,-2,-70,-66,0,0,0,52,0,40,10,0,9,0,24,10,0,25,0,26,8,0,27,10,0,25,0,28,7,0,29,
            7,0,30,10,0,6,0,31,7,0,32,7,0,33,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,18,76,111,99,97,108,86,97,114,105,97,98,108,101,
            84,97,98,108,101,1,0,4,116,104,105,115,1,0,3,76,66,59,1,0,8,60,99,108,105,110,105,116,62,1,0,1,101,1,0,21,
            76,106,97,118,97,47,105,111,47,73,79,69,120,99,101,112,116,105,111,110,59,1,0,13,83,116,97,99,107,77,97,112,
            84,97,98,108,101,7,0,29,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,6,66,46,106,97,118,97,12,0,10,0,11,7,
            0,34,12,0,35,0,36,1,0,4,99,97,108,99,12,0,37,0,38,1,0,19,106,97,118,97,47,105,111,47,73,79,69,120,99,101,
            112,116,105,111,110,1,0,26,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,69,120,99,101,112,
            116,105,111,110,12,0,10,0,39,1,0,1,66,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,17,
            106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,1,0,10,103,101,116,82,117,110,116,105,109,101,
            1,0,21,40,41,76,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,59,1,0,4,101,120,101,99,1,0,39,
            40,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,76,106,97,118,97,47,108,97,110,103,47,
            80,114,111,99,101,115,115,59,1,0,24,40,76,106,97,118,97,47,108,97,110,103,47,84,104,114,111,119,97,98,108,101,
            59,41,86,0,33,0,8,0,9,0,0,0,0,0,2,0,1,0,10,0,11,0,1,0,12,0,0,0,47,0,1,0,1,0,0,0,5,42,-73,0,1,-79,0,0,0,2,0,13,
            0,0,0,6,0,1,0,0,0,9,0,14,0,0,0,12,0,1,0,0,0,5,0,15,0,16,0,0,0,8,0,17,0,11,0,1,0,12,0,0,0,102,0,3,0,1,0,0,0,23,
            -72,0,2,18,3,-74,0,4,87,-89,0,13,75,-69,0,6,89,42,-73,0,7,-65,-79,0,1,0,0,0,9,0,12,0,5,0,3,0,13,0,0,0,22,0,5,
            0,0,0,12,0,9,0,15,0,12,0,13,0,13,0,14,0,22,0,16,0,14,0,0,0,12,0,1,0,13,0,9,0,18,0,19,0,0,0,20,0,0,0,7,0,2,76,
            7,0,21,9,0,1,0,22,0,0,0,2,0,23
    };
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("调用了自定义的Classloader");
        if (name.equals(ClassName)) {
            System.out.println(ClassName);
            return defineClass(ClassName, bytes, 0, bytes.length);
        }
        return super.findClass(name);
    }
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        UserClassloaderTest userClassloaderTest = new UserClassloaderTest();
        Class<?> aClass = userClassloaderTest.loadClass("B");
        Object newInstance = aClass.newInstance();
    }
}

image-20231029184041013

这里 byte 中的内容是下面这段代码编译后的 class 文件得到的

import java.io.IOException;
/**
 * @BelongsProject: study_java
 * @BelongsPackage: PACKAGE_NAME
 * @Author: Clown
 * @CreateTime: 2023-10-11  19:52
 */
public class B {
    static  {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

然后通过下面的代码来得到 Byte 中的内容

public class test{
    public static void main(String[] args) throws Exception{
        String filePath = "E:\\study_java\\target\\classes\\B.class";
        try (FileInputStream fis = new FileInputStream(filePath)) {
            byte[] data = new byte[fis.available()]; // 创建一个字节数组,大小等于文件大小
            fis.read(data); // 读取文件内容到字节数组
            // 打印字节数据
            for (byte b : data) {
                System.out.print(b + ",");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

这里在测试上面的代码的时候要注意将原本的 B 类注释掉

# URLClassLoader

UrlClassLoader 其实在前面 CC3 的分析中已经有提到也用过 ,但是这并不影响我再水一遍,其中默认的类加载器的继承关系

它是 AppClassLoader 的父类实际上也是继承了 ClassLoader,这个 URLClassLoader 实际上所有的 jar 包都是通过这个加载器来加载的,使用 file 协议这种去加载的。当然在前面的 CC3 中就已经器加载远程的文件

package ClassLoaderStudy;
import java.net.URL;
import java.net.URLClassLoader;
/**
 * @BelongsProject: study_java
 * @BelongsPackage: ClassLoaderStudy
 * @Author: Clown
 * @CreateTime: 2023-10-31  13:56
 */
public class URLClassLoaderTest {
    public static void main(String[] args) throws Exception{
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("Http://127.0.0.1:8000/")});
        Class<?> c = urlClassLoader.loadClass("B");
        c.newInstance();
    }
}

image-20231031135834696

这里和上面一样,也是需要将项目中的这个文件给注释掉,不然这里还是会先加载本地的

# 类加载隔离

创建类加载的时候是可以指定该加载类的父类加载器的,比如说上面的 URLClassLoader 这个类中

image-20231031142001591

他是有参数可以指定其父类加载器的

package ClassLoaderStudy;
/**
 * @BelongsProject: study_java
 * @BelongsPackage: ClassLoaderStudy
 * @Author: Clown
 * @CreateTime: 2023-10-31  14:33
 */
public class ClassLoaderIsolate {
    public static String ClassName = "B";
    public static byte[] bytes = new byte[]{-54,-2,-70,-66,0,0,0,52,0,40,10,0,9,0,24,10,0,25,0,26,8,0,27,10,0,25,0,28,7,0,29,
            7,0,30,10,0,6,0,31,7,0,32,7,0,33,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,18,76,111,99,97,108,86,97,114,105,97,98,108,101,
            84,97,98,108,101,1,0,4,116,104,105,115,1,0,3,76,66,59,1,0,8,60,99,108,105,110,105,116,62,1,0,1,101,1,0,21,
            76,106,97,118,97,47,105,111,47,73,79,69,120,99,101,112,116,105,111,110,59,1,0,13,83,116,97,99,107,77,97,112,
            84,97,98,108,101,7,0,29,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,6,66,46,106,97,118,97,12,0,10,0,11,7,
            0,34,12,0,35,0,36,1,0,4,99,97,108,99,12,0,37,0,38,1,0,19,106,97,118,97,47,105,111,47,73,79,69,120,99,101,
            112,116,105,111,110,1,0,26,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,69,120,99,101,112,
            116,105,111,110,12,0,10,0,39,1,0,1,66,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,17,
            106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,1,0,10,103,101,116,82,117,110,116,105,109,101,
            1,0,21,40,41,76,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,59,1,0,4,101,120,101,99,1,0,39,
            40,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,76,106,97,118,97,47,108,97,110,103,47,
            80,114,111,99,101,115,115,59,1,0,24,40,76,106,97,118,97,47,108,97,110,103,47,84,104,114,111,119,97,98,108,101,
            59,41,86,0,33,0,8,0,9,0,0,0,0,0,2,0,1,0,10,0,11,0,1,0,12,0,0,0,47,0,1,0,1,0,0,0,5,42,-73,0,1,-79,0,0,0,2,0,13,
            0,0,0,6,0,1,0,0,0,9,0,14,0,0,0,12,0,1,0,0,0,5,0,15,0,16,0,0,0,8,0,17,0,11,0,1,0,12,0,0,0,102,0,3,0,1,0,0,0,23,
            -72,0,2,18,3,-74,0,4,87,-89,0,13,75,-69,0,6,89,42,-73,0,7,-65,-79,0,1,0,0,0,9,0,12,0,5,0,3,0,13,0,0,0,22,0,5,
            0,0,0,12,0,9,0,15,0,12,0,13,0,13,0,14,0,22,0,16,0,14,0,0,0,12,0,1,0,13,0,9,0,18,0,19,0,0,0,20,0,0,0,7,0,2,76,
            7,0,21,9,0,1,0,22,0,0,0,2,0,23
    };
    public static class ClassLoaderA extends ClassLoader {
        public ClassLoaderA(ClassLoader parent) {
            super(parent);
        }
        {
            // 加载类字节码
            defineClass(ClassName, bytes, 0, bytes.length);
        }
    }
    public static class ClassLoaderB extends ClassLoader {
        public ClassLoaderB(ClassLoader parent) {
            super(parent);
        }
        {
            // 加载类字节码
            defineClass(ClassName, bytes, 0, bytes.length);
        }
    }
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        ClassLoaderA classLoaderA = new ClassLoaderA(classLoader);
        ClassLoaderB classLoaderB = new ClassLoaderB(classLoader);
        Class classa = Class.forName(ClassName,true,classLoaderA);
        Class classaa = Class.forName(ClassName,true,classLoaderA);
        Class classb = Class.forName(ClassName,true,classLoaderB);
        System.out.println("classa == classaa"+"======"+(classa == classaa));
        System.out.println("classa == classb" +"======"+(classa == classb));
//        System.out.println(classa.getName());
    }
}

image-20231031144611873

ClassLoader A 和 ClassLoader B 可以加载相同类名的类,但是 ClassLoader A 中的 Class A 和 ClassLoader B 中的 Class A 是完全不同的对象,两者之间调用只能通过反射

# BCEL ClassLoader

这个 BCEL 的 ClassLoader 实际上在前面记录 FastJson 反序列化漏洞的时候就已经了解过连接:

FastJson 漏洞分析 - java 安全 | Clown の Blog = (xcu.icu)

BCELApache Commons BCEL™ )是一个用于分析、创建和操纵 Java 类文件的工具库,Oracle JDK 引用了 BCEL 库,不过修改了原包名 org.apache.bcel.util.ClassLoader 为 com.sun.org.apache.bcel.internal.util.ClassLoader,也就是说这里实际上是不需要依赖的

image-20231031153139417

在其 ClassLoader 方法中这里会有一个判断,如果类名中存在 $$BCEL$$ 就会调用这个 createClass 方法

image-20231031153356451

这里会将这个类名当做类加载出来,简单来说只需要类名就可以得到一个类,又是 sun 包下的内容

这个特性在 BCEL6.0 以下支持

# Xalan ClassLoader

Oracle JDK 默认也引用了 Xalan,同时修改了原包名 org.apache.xalan.xsltc.trax.TemplatesImpl 为 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,没错就是我们常用的这个 TemplatesImpl 类。TemplatesImpl 可以传入类字节码并初始化。这里也可以看到,它与上面的 BCEL 需要进行一次加密是不一样的,这里值需要直接将字节码传入即可。当然在 FastJson 漏洞分析 - java 安全 | Clown の Blog = (xcu.icu) 我也是有记录到的

在 FastJson 里利用实际上就是在 com.sun.org.apache.xalan.internal.xsltc.trax 类中的 getTransletInstance 方法中

image-20231011181254063

这里有一个 newInstance 方法可以实实例化对象,这里看一下 _class [_transletIndex] 是不是可控的

image-20231011181948972

这里可以看到其实在当前类的 defineTransletClasses 方法中,这里可以看到,这里的_transletIndex 是可以根据_bytecodes 改变的

image-20231011182146842

而在本类下,还有对应的 set 和 get 方法,很显然是可控的,这里我们就需要看一下 defineTransletClasses 方法

image-20231011182426758

首先这里会判断_bytecodes 是否为空,这里可控,所以这里我们可以传入一个恶意的类

image-20231011182529594

_bytecodes 不为空,运行到这里,此时,需要满足_tfactory 变量不为 null,否则导致程序异常退出。

image-20231011182735816

这个成员属性没有对应的 set 和 get 方法,这里需要指定 Feature.SupportNonPublicField 参数来为_tfactory 赋值

image-20231011182940756

把_bytecodes 数组中的值循环取出,使用 loader.defineClass 方法从字节码转化为 Class 对象,随后后赋值给_class [i],如果循环到 main class,就会将_transletIndex 变量值赋值为_bytecodes 数组中的下标值

image-20231011183255470

在回到这个入口类,当前想要执行到 newInstance 方法,只需要_name 不为 null 即可

image-20231011183402004

它有对应的 set 方法,到这里条件就都满足了

利用条件十分之苛刻