类加载器(ClassLoader)
类加载器(ClassLoader)
类加载、编译#
类加载器用于将Java类(此时的Java类指的是已经从.java
编译成.class
的字节码文件)通过JVM加载到内存中才能运行。
编译java文件
包名为:
package com.melody.sec.classloader;
、类名称为:DefineClassDemo
-
编译java文件
javac com/melody/sec/classloader/DefineClassDemo.java
-
运行class文件
java com.melody.sec.classloader.DefineClassDemo
-
反编译class文件
javap -c -l -p com.melody.sec.classloader.DefineClassDemo
-
查看字节码具体内容
hexdump -C com/melody/sec/classloader/DefineClassDemo.class
请注意:
在java程序运行时涉及到类的概念,因此在运行class文件的时候使用的是类修饰限定的形式,而当在编译代码,查看文件内容的具体情况时,使用的是相对路径。「总结:当作文件利用时写文件名,当作类使用时写类名」
类加载器#
类加载器有四类(Bootstrap ClassLoader(引导类加载器)
、Extension ClassLoader(扩展类加载器)
、App ClassLoader(系统类加载器)
),在其他文章中有明确解释,以及父类委派机制的流程,这里不在赘述。
父类委派机制:设计的初衷是为了防止攻击者恶意修改一些标准库中的类文件,因此通过父类委派机制对常规使用的类进行加载,防止其他人的恶意破坏。
「总结:对于某个类名(the binary name of a class
),总是先使用父类加载器对其进行加载,若无法加载在使用子类的类加载器」。
the binary name of a class, for example :
"java.lang.String" "javax.swing.JSpinner$DefaultEditor" "java.security.KeyStore$Builder$FileBuilder$1" "java.net.URLClassLoader$3$1"
类加载器加载类的逻辑:
类名 -> 文件名->获取对应的class文件->读取class文件中字节码->将字节码加载到JVM中
理解了类加载器加载类的逻辑后,不妨提出几个问题:
- 如何获取类加载器,获取的类加载器是属于哪一种?
- 类加载器如何加载指定的类?
- 所有的类都有对应的类加载器吗?
下面,对上述问题进行逐一的解决。
获取类加载器#
每一个类对象都包含了定义它的类加载器的引用。因此,获取类加载器的思路应该是从类对象中来。
-
获取当前类的类加载器
this.getClass().getClassLoader(); // sun.misc.Launcher$AppClassLoader@18b4aac2
-
获取其他包中的类的类加载器
ZipPath.class.getClassLoader(); // sun.misc.Launcher$ExtClassLoader@28d93b30
-
获取JVM在初始化时就加载的类的类加载器,即
Bootstrap ClassLoader
加载的类File.class.getClassLoader(); // null
所有类都有类加载器吗?#
知道如何获取类加载器后,可以解决问题三,所有的类都有对应的类加载器吗?
查看官方文档,存在两种「特例」:
-
类数组加载时并不会使用类加载器进行加载,而是依赖于Java Runtime,但对数组类获取类加载器时,和对数据中的每个元素的类加载器是一致的。
public void solveProblem03(){ // 所有的类都有对应的类加载器吗? // Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; DefineClassDemo[] classDemos = new DefineClassDemo[3]; ClassLoader classLoader = classDemos.getClass().getClassLoader(); System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2 }
-
若类是元类型,则没有类加载器
// if the element type is a primitive type, then the array class has no class loader. char[] bytes = "hello".toCharArray(); System.out.println(bytes.getClass().getClassLoader()); // null
类加载器如何加载类#
类从哪里加载?
- 从文件系统加载类。(如CLASSPATH环境变量定义的类路径加载)
- 从远端服务器上加载类。
类如何加载?
-
隐式加载:通过new等途径生成对象,通过JVM把相应的类加载到内存中
「相当于某工厂接到了一个订单,然后工人根据这个订单的列表去找对应的模具,放到生产线生产出对应的商品交付给顾客」
new 类实例 或者 类名.方法名
-
显式加载:通过
Class.forName
的方式由程序员自己控制加载「相当于工厂工人能够利用工厂中的模具、原料,自己制造自己想做的东西」
// 使用Class.forName加载示例 TestHelloWorld obj = (TestHelloWorld) Class.forName("com.melody.sec.classloader.TestHelloWorld").newInstance(); obj.sayHello(); // 使用ClassLoader加载示例 TestHelloWorld obj = (TestHelloWorld)DefineClassDemo.class.getClassLoader().loadClass("com.melody.sec.classloader.TestHelloWorld").newInstance(); obj.sayHello();
此时,我们知道如何利用默认的类加载器加载一些类,我们也可以利用类可继承的特性,创造出属于我们自己的ClassLoader
自定义类加载器#
在上文中讲到,类加载器不仅仅可以从文件系统中加载字节码将类注入到JVM中,也可以通过网络的方式,其中一个有名的类叫:URLClassLoader
。首先我们先完成一个简单的自定义类加载器,然后理解了如何自己写一个自定义类加载器后,再尝试理解URLClassLoader
。
-
写一个恶意类,定义一个方法返回恶意字符串
package com.melody.sec.classloader; /** * @author Me1ody * @version 1.0 */ public class EvilClass { public String sayHello(){ return "EvilClass has been injected!"; } }
-
编译该类为字节码文件
-
将字节码文件读取出来并转换为
byte[]
数组(方法)public static String evilFilename = "绝对路径/com/melody/sec/classloader/EvilClass.class"; public static void main(String[] args) { try { FileInputStream fileInputStream = new FileInputStream(evilFilename); // 文件读入流 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); // 字节输出流 int length; byte[] buffer = new byte[1024]; while ((length = fileInputStream.read(buffer)) != -1) // -1表示文件读取结束 { outputStream.write(buffer, 0, length); } byte[] bytecodes = outputStream.toByteArray(); for (byte b: bytecodes) { System.out.print(b + ", "); } } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } }
我们自定义的类加载器的思路是,假设我们的类路径下并没有这个EvilClass
类,正常情况下使用类加载器去加载时一定会显示类名不存在的错误,我们的自定义类加载器若收到类名为EvilClass
时,却可以正常的加载到JVM中。
为了实现这个自定义类加载器,我们需要完成两件事:
- 编写一个新的类,继承
java.lang.ClassLoader
- 重写
ClassLoader
的findClass
方法
具体代码如下:
package com.melody.sec.classloader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author Me1ody
* @version 1.0
* 自定义类加载器
* 1. 继承父类 ClassLoader类
* 2. 重写findClass方法
*/
public class MyOwnClassLoader extends ClassLoader{
private static String className = "com.melody.sec.classloader.EvilClass";
private static byte[] codes = 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 {
// 处理EvilClass类
if (name.equals(className))
{
return defineClass(className, codes, 0, codes.length);
}
return super.findClass(name);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
// 初始化自定以类加载器对象
MyOwnClassLoader loader = new MyOwnClassLoader();
// 1、加载类
Class<?> aClass = loader.loadClass(className);
// 2、实例化
Object obj = aClass.newInstance();
// 3、反射获取方法
Method sayHello = obj.getClass().getMethod("sayHello");
// 4、执行方法
String retString =(String) sayHello.invoke(obj);
// 5、打印返回结果
System.out.println(retString);
}
}
「利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测,也可以用于加密重要的Java类字节码(只能算弱加密了)。」(后续将补充具体的应用)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!