ClassLoader 概述
一、什么是 ClassLoader
ClassLoader 是一个负责将 .class 文件加载到 JVM 中的组件。Java 默认的 ClassLoader 就是根据类名来加载类,这个类名是类完整路径,如 java.lang.Runtime
Java 类并不是一次性将所有类全部加载到内存中的,而是在应用程序需要时才加载。此时,JRE 会调用 Java 类加载器,这些类加载器会将类动态加载到内存中,从而提高了应用程序的灵活性和运行效率
二、ClassLoader 的特性
- 动态加载类: Java 具有动态加载类的特性。只有当程序真正需要使用某个类时,ClassLoader 才会去加载它。这有助于减少 JVM 启动时的内存占用,并提高程序的灵活性
- 类隔离: 不同的 ClassLoader 可以加载相同名称的类,并且这些类在 JVM 中被认为是不同的类型。这为实现应用程序的模块化和隔离提供了基础。例如,在 Web 服务器中,不同的 Web 应用可以使用各自的 ClassLoader,从而避免类名冲突
- 扩展性: Java 允许开发者自定义 ClassLoader,以实现从特殊来源加载类、对类进行加密解密等功能。这为 Java 平台的扩展性提供了强大的支持
- 实现某些高级特性: 像热部署、插件化等高级特性都依赖于 ClassLoader 的机制
三、ClassLoader 的层次结构
Java 的类加载器分为不同的类型,每种类型负责从特定的位置加载类
JVM 采用了一种称为 双亲委派模型 (Parent-Delegation Model) 的机制来管理 ClassLoader。这个模型形成了一个层次结构,包含以下三种 ClassLoader 类型:
-
Bootstrap ClassLoader (启动类加载器):
- 这是最顶层的 ClassLoader,是 JVM 底层实现的一部分
- 负责加载 JVM 启动时需要的核心类库,例如
java.lang.*
、java.util.*
等 - 在 JDK 8 及以前,它从 rt.jar 加载核心 Java 文件。JDK 9 之后从 Java runtime image 加载核心 Java 文件
- 没有父 ClassLoader
-
Platform ClassLoader (Extension ClassLoader):
- JDK 8 及之前有 Extension ClassLoader(扩展类加载器),JDK 9 后被替换为 Platform ClassLoader(平台类加载器)
- 从 JDK 的模块系统加载特定于平台的扩展
- 父加载器是 Bootstrap ClassLoader
-
System ClassLoader (系统类加载器)(也称 Application ClassLoader (应用程序类加载器)):
- 是 Platform ClassLoader 的子类
- 负责加载应用程序 classpath 下的类,包括用户自定义的类和第三方库
- System ClassLoader 是应用程序默认使用的 ClassLoader
- 父加载器是 Platform ClassLoader
它们不是父子类继承关系,而是委托关系
除了以上三个主要的 ClassLoader,开发者还可以根据需要创建自定义 ClassLoader。自定义 ClassLoader 通常是 System ClassLoader 的子加载器
双亲委派模型的工作流程:
当一个 ClassLoader 收到加载类的请求时,它不会首先自己去尝试加载,而是将这个请求委派给它的父加载器。这个委派过程会一直向上进行,直到到达最顶层的 Bootstrap ClassLoader
- 当一个 ClassLoader 收到类加载请求
- 它会将这个请求委派给它的父加载器
- 父加载器也会将请求委派给它的父加载器,如此递归向上
- 直到委派给最顶层的 Bootstrap ClassLoader
- Bootstrap ClassLoader 尝试加载该类
- 如果加载成功,则返回加载好的 Class 对象
- 如果加载失败(在其负责的范围内没有找到该类),则将加载请求向下传递给它的子加载器(即发起这个请求的 ClassLoader)
- 子加载器尝试加载该类
- 如果加载成功,则返回加载好的 Class 对象
- 如果加载失败,则继续尝试其它的子加载器
- 如果最终没有任何 ClassLoader 能够加载该类,则会抛出
ClassNotFoundException
或NoClassDefFoundError
异常
双亲委派模型的好处:
- 安全性: 避免了用户自定义的类替换核心类库中的类。例如,用户无法自定义一个名为
java.lang.String
的类,因为java.lang
包下的类总是由 Bootstrap ClassLoader 优先加载的 - 避免类的重复加载: 当父加载器已经加载过某个类时,子加载器就不会再重复加载,保证了 JVM 中同一个类只有一个 Class 对象
四、类加载的过程
一个类从被加载到 JVM 中到被卸载会经历以五个阶段:
-
加载 (Loading):
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为这个类的各种数据的访问入口
在加载阶段,不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用:
ClassLoader#loadClass --> ClassLoader#findClass --> ClassLoader#defineClass
loadClass
的作用是从已加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下,执行 findClassfindClass
作用是根据特定的来源(本地文件、jar 包、远程服务器等)加载类的字节码,然后交给defineClass
。这是一个抽象方法,通常需要自定义类加载器时进行重写,以实现从特定来源加载类的逻辑defineClass
接收findClass
加载到的字节码,并将其转换为 JVM 可以识别和使用的 Class 对象。这个方法由 JVM 底层实现
-
连接 (Linking):
- 验证 (Verification): 确保 Class 文件的字节流中包含的信息符合当前 JVM 的要求,并且不会危害 JVM 自身的安全。
- 准备 (Preparation): 为类变量(static 变量)分配内存,并设置类变量的初始值(通常是零值)。注意,这里不包含实例变量,实例变量会在对象实例化时在堆中分配内存。
- 解析 (Resolution): 将常量池中的符号引用替换为直接引用。直接引用是指向目标内存地址的指针、相对偏移量或者是一个能直接定位到目标的句柄。解析阶段可以在初始化之后再开始,这被称为延迟解析。
-
初始化 (Initialization):
- 执行类构造器
<clinit>()
方法的过程。<clinit>()
方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{}
)中的语句合并产生的。 - 如果该类存在父类,并且父类还没有被初始化,则先触发其父类的初始化。
- JVM 保证在子类的
<clinit>()
方法执行之前,父类的<clinit>()
方法已经执行完毕。 - 如果一个类中没有对类变量进行赋值操作,也没有静态语句块,那么编译器可以不为这个类生成
<clinit>()
方法。 - 接口中不能使用静态语句块,但仍然可以有变量初始化的赋值操作,因此接口也会生成
<clinit>()
方法。不过,接口的<clinit>()
方法与类的<clinit>()
方法有所不同,执行接口的<clinit>()
方法不需要先执行父接口的<clinit>()
方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。
- 执行类构造器
-
使用 (Using): 类被成功加载、连接和初始化后,就可以在程序中正常使用了
-
卸载 (Unloading): 当一个类不再被任何存活的对象引用,并且 JVM 的垃圾回收器也没有回收该类的 Class 对象时,该类可能会被卸载。在实际应用中,类的卸载条件比较苛刻,通常只有在动态加载和热替换的场景下才比较容易发生。
URLClassLoader
URLClassLoader
实际上是我们平时默认使用的AppClassLoader
的父类,所以解释URLClassLoader
的工作过程实际上就是在解释默认的 Java 类加载器的工作流程正常情况下,Java会根据配置项
sun.boot.class.path
和java.class.path
中列举到的基础路径(这些路径是经过处理后的java.net.URL
类)来寻找 .class 文件来加载,而这个基础路径有分为三种情况:
- URL未以斜杠结尾,则认为是一个JAR文件,使用 JarLoader来寻找类,即为在Jar包中寻找 .class 文件
- URL以斜杠结尾,且协议名是 file,则使用 FileLoader来寻找类,即为在本地文件系统中寻找 .class 文件
- URL以斜杠结尾,且协议名不是 file,则使用最基础的 Loader来寻找类
我们正常开发的时候通常遇到的是前两者,那什么时候才会出现使用 Loader寻找类的情况呢?当然是非 file协议的情况下,最常见的就是 http协议
示例:
1. 被加载的类
public class RemoteClass {
public RemoteClass() {
System.out.println("RemoteClass loaded");
}
}
2. 提供类的服务器
import http.server
import socketserver
import os
PORT = 8000
CLASS_FILE_PATH = 'RemoteClass.class'
if not os.path.exists(CLASS_FILE_PATH):
print(f"Error: File '{CLASS_FILE_PATH}' does not exist")
exit()
class ClassFileHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/' + CLASS_FILE_PATH:
try:
with open(CLASS_FILE_PATH, 'rb') as f:
self.send_response(200)
self.send_header('Content-type', 'application/java-vm')
self.send_header('Content-Disposition', f'attachment; filename="{CLASS_FILE_PATH}"')
self.end_headers()
self.wfile.write(f.read())
except FileNotFoundError:
self.send_error(404, "File not found")
except Exception as e:
self.send_error(500, f"Internal Server Error: {e}")
else:
super().do_GET()
if __name__ == "__main__":
with socketserver.TCPServer(("", PORT), ClassFileHandler) as httpd:
print(f"Providing '{CLASS_FILE_PATH}' at port {PORT}...")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped")
3. 加载类
注意:同一个项目里不要有被加载的 class,影响测试(找不到的时候会从本地对应包名加载)
package com.kkayu;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
URL[] urls = {new URL("http://localhost:8000/")};
URLClassLoader loader = new URLClassLoader(urls);
Class cls = loader.loadClass("RemoteClass");
cls.newInstance();
}
}
ClassLoader#defineClass 直接加载字节码
ClassLoader#defineClass 是 Java 中 ClassLoader 类的一个 protected 核心方法。它的主要作用是将原始的字节码数据转换为 JVM 可以识别和使用的 java.lang.Class 类的实例
简单来说,当你实现一个自定义的类加载器时,你通常会重写 findClass 方法来查找或生成类的字节码。一旦你获得了字节码,你就需要调用 defineClass 方法来将这些字节码“定义”为一个真正的 Java 类
示例:
编译一个 class,然后通过 defineClass 可以直接加载
1. 被加载的类
public class CustomClass {
public CustomClass() {
System.out.println("CustomClass loaded");
}
}
编译后转换为 base64
cat CustomClass.class | base64 | tr -d "\n"
2. 加载类
此处用 JDK 8 测试
package com.kkayu;
import java.lang.reflect.Method;
import java.util.Base64;
class DefineClassExample {
public static void main(String[] args) throws Exception {
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Base64.getDecoder().decode("<字节码的 base64 值>");
Class customClass = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "CustomClass", code, 0, code.length);
customClass.newInstance();
}
}
defineClass 方法参数说明:
- name – The expected binary name of the class, or null if not known
- b – The bytes that make up the class data. The bytes in positions off through off+len-1 should have the format of a valid class file as defined by The Java™ Virtual Machine Specification.
- off – The start offset in b of the class data
- len – The length of the class data
因为系统的 ClassLoader#defineClass 是一个 protected 属性,所以我们无法直接在外部访问,不得不使用反射的形式来调用。在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石
TemplatesImpl 加载字节码
虽然大部分上层开发者不会直接使用到 defineClass
方法,但 Java 底层还是有一些类用到了它,这就是 TemplatesImpl
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类中定义了一个内部类 TransletClassLoader
:
static final class TransletClassLoader extends ClassLoader {
private final Map < String, Class > _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent, Map < String, Class > mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class < ? > loadClass(String name) throws ClassNotFoundException {
Class < ? > ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret =
_loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
/**
Access to final protected superclass member from outer class.
**/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
在 idea 里看这个类可以按快捷键
Ctrl + N
或Command + O
使用 "Go to Class" 功能,输入完整类名
这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为 default。所以也就是说这里的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用
从 TransletClassLoader#defineClass()
向前追溯一下调用链:
TemplatesImpl#getOutputProperties() -->
TemplatesImpl#newTransformer() -->
TemplatesImpl#getTransletInstance() -->
TemplatesImpl#defineTransletClasses() -->
TransletClassLoader#defineClass()
追溯调用链可以用一些快捷键:
- Ctrl + B / Command + B - Go to Definition/Declaration
- Alt + F7 / Option + F7 - Find Usage (默认只在项目文件里查找,建议在搜索设置里设置 Scope: All Places)
追到最前面两个方法 TemplatesImpl#getOutputProperties()
、TemplatesImpl#newTransformer()
,这两者的作用域是 public,可以被外部调用。粗略看一下调用链的代码,尝试用 newTransformer()
构造一个简单的 POC:
package com.kkayu;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import java.util.Base64;
import static cn.hutool.core.bean.BeanUtil.setFieldValue;
public class TemplatesImplExample {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAHgoABgAQCQARABIIABMKABQAFQcAFgcAFwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTEN1c3RvbUNsYXNzOwEAClNvdXJjZUZpbGUBABBDdXN0b21DbGFzcy5qYXZhDAAHAAgHABgMABkAGgEAEkN1c3RvbUNsYXNzIGxvYWRlZAcAGwwAHAAdAQALQ3VzdG9tQ2xhc3MBABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAD8AAgABAAAADSq3AAGyAAISA7YABLEAAAACAAoAAAAOAAMAAAACAAQAAwAMAAQACwAAAAwAAQAAAA0ADAANAAAAAQAOAAAAAgAP");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] { code });
setFieldValue(obj, "_name", "NOT NULL");
obj.newTransformer();
}
}
然后运行抛出了空指针异常
Exception in thread "main" java.lang.NullPointerException
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$1.run(TemplatesImpl.java:401)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:399)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.kkayu.TemplatesImplExample.main(TemplatesImplExample.java:16)
在异常处打断点,发现是因为 defineTransletClasses
里面调用了 _tfactory.getExternalExtensionsMap()
Ctrl 点一下查看它的声明类型是 TransformerFactoryImpl
,给它赋个 TransformerFactoryImpl
对象
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Alt + Shift + F7 / Option + Shift + F7 强制步入
然后发现又一个空指针异常,但是在异常处打断点会发现调用链已经过了,却什么都没有发生
再在中间调试,可以看到类已经加载了,生成了 Class
对象。推测什么都没发生的原因是加载的类还没有初始化
从 defineTransletClasses()
中的 _class[i] = loader.defineClass(_bytecodes[i]);
可以看到类被加载放到了 _class
数组中(它的类型是 Class[]
)。然后 Find Usage,在 Value Read 里看到了调用了数组元素的 newInstance()
另外,这个对象类型是 AbstractTranslet,来自 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
,但是强制类型转换发生在对象实例化之后,在此之前并不是一定要是这个类
实例化发生在调用 defineTransletClasses()
之后,然而空指针异常是这个函数中抛出的,所以要解决这个异常
空指针的原因是 _auxClasses
的值是 null。此处可以让它不进入 else 块,构造一个 AbstractTranslet 类转换成字节码让给它加载。除此之外用 setFieldValue 给引发空指针异常的对象成员赋值也可以解决这个问题,从而可以加载任意类而不局限于 AbstractTranslet:
import java.util.HashMap;
setFieldValue(obj, "_auxClasses", new HashMap<String, Class<?>>());
setFieldValue(obj, "_transletIndex", 0);
这里还设置了 _transletIndex
的值为 0(如果设置成更大的数字会抛出数组越界的异常),因为紧接着 for 循环结束后有一个 if 判断小于 0 就抛出异常
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
没有经过 for 循环里 if 块,_transletIndex 依然是初始值 0
完整 poc:
package com.kkayu;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.util.Base64;
import static cn.hutool.core.bean.BeanUtil.setFieldValue;
import java.util.HashMap;
public class TemplatesImplExample {
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAHgoABgAQCQARABIIABMKABQAFQcAFgcAFwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTEN1c3RvbUNsYXNzOwEAClNvdXJjZUZpbGUBABBDdXN0b21DbGFzcy5qYXZhDAAHAAgHABgMABkAGgEAEkN1c3RvbUNsYXNzIGxvYWRlZAcAGwwAHAAdAQALQ3VzdG9tQ2xhc3MBABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAD8AAgABAAAADSq3AAGyAAISA7YABLEAAAACAAoAAAAOAAMAAAACAAQAAwAMAAQACwAAAAwAAQAAAA0ADAANAAAAAQAOAAAAAgAP");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] { code });
setFieldValue(obj, "_name", "NOT NULL");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
setFieldValue(obj, "_auxClasses", new HashMap<String, Class<?>>());
setFieldValue(obj, "_transletIndex", 0);
obj.newTransformer();
}
}
运行输出:
CustomClass loaded
Exception in thread "main" java.lang.ClassCastException: CustomClass cannot be cast to com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:455)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.kkayu.TemplatesImplExample.main(TemplatesImplExample.java:19)
Process finished with exit code 1
类实例化后成功执行了预期的代码,紧接着是 ClassCastException,这是因为源码调用了 newInstance 后尝试强制把实例化的自定义类转换为 AbstractTranslet 类
另一种方法是构造 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类(AbstractTranslet 是抽象类)让程序进入 if 块避免异常
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class CustomTransletClass extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,SerializationHandler handler) throws TransletException {}
public CustomTransletClass() {
super();
System.out.println("CustomTransletClass loaded");
}
}
这样的话不需要设置那两个属性值:
package com.kkayu;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.util.Base64;
import static cn.hutool.core.bean.BeanUtil.setFieldValue;
import java.util.HashMap;
public class TemplatesImplExample {
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQALAoABgAdCQAeAB8IACAKACEAIgcAIwcAJAEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAVTEN1c3RvbVRyYW5zbGV0Q2xhc3M7AQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHACUBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABhDdXN0b21UcmFuc2xldENsYXNzLmphdmEMABkAGgcAJgwAJwAoAQAaQ3VzdG9tVHJhbnNsZXRDbGFzcyBsb2FkZWQHACkMACoAKwEAE0N1c3RvbVRyYW5zbGV0Q2xhc3MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAAD8AAAADAAAAAbEAAAACAAoAAAAGAAEAAAAIAAsAAAAgAAMAAAABAAwADQAAAAAAAQAOAA8AAQAAAAEAEAARAAIAEgAAAAQAAQATAAEABwAUAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAAJAAsAAAAqAAQAAAABAAwADQAAAAAAAQAOAA8AAQAAAAEAFQAWAAIAAAABABcAGAADABIAAAAEAAEAEwABABkAGgABAAkAAAA/AAIAAQAAAA0qtwABsgACEgO2AASxAAAAAgAKAAAADgADAAAACwAEAAwADAANAAsAAAAMAAEAAAANAAwADQAAAAEAGwAAAAIAHA==");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] { code });
setFieldValue(obj, "_name", "NOT NULL");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
// setFieldValue(obj, "_auxClasses", new HashMap<String, Class<?>>());
// setFieldValue(obj, "_transletIndex", 0);
obj.newTransformer();
}
}
补充一点,在反序列化中可以不用手动设置 _tfactory
的值,因为在 TemplatesImpl#readObject
中有 _tfactory = new TransformerFactoryImpl();
,也就是说在反序列化的时候会被设置好
BCEL ClassLoader 加载字节码
BCEL 的全称是 Apache Commons BCEL,它是 Apache Commons 项目的一个子项目。由于 Apache Xalan 使用了 BCEL,而 Apache Xalan 又是 Java 内部 JAXP(Java API for XML Processing)的实现,因此 BCEL 也被包含在了 JDK 的原生库中
BCEL ClassLoader 来自 com.sun.org.apache.bcel.internal.util.ClassLoader
,用于加载 BCEL 字节码。但它在 Java 8u251 之后被移除了
从它的源码中可以看到重写了 loadClass
方法:
protected Class loadClass(String class_name, boolean resolve) throws ClassNotFoundException {
Class cl = null;
// First try: lookup hash table.
cl = (Class) classes.get(class_name);
if (cl == null) {
// Second try: Load system class using system class loader. You better don't mess around with them.
for (int i = 0; i < ignored_packages.length; i++) {
if (class_name.startsWith(ignored_packages[i])) {
cl = deferTo.loadClass(class_name);
break;
}
}
if (cl == null) {
JavaClass clazz = null;
// Third try: Special request?
if (class_name.indexOf("$$BCEL$$") >= 0) {
clazz = createClass(class_name);
} else { // Fourth try: Load classes via repository
clazz = repository.loadClass(class_name);
if (clazz != null) {
clazz = modifyClass(clazz);
} else {
throw new ClassNotFoundException(class_name);
}
}
if (clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else { // Fourth try: Use default class loader
cl = Class.forName(class_name);
}
}
if (resolve) {
resolveClass(cl);
}
}
classes.put(class_name, cl);
return cl;
}
源码注释中注明了四次加载尝试,简单梳理一下这个过程:
- 首先尝试从 classes(哈希表)中查找是否已经加载过这个类。如果找到了就直接返回
- 如果缓存中没有找到,方法会进行第二次尝试:加载系统类。它会遍历
ignored_packages
的字符串数组,这个数组里存放着一些包名的前缀("java.", "javax.", "sun."
)。如果要加载的类名以这些前缀开头,那么这个类就被认为是系统类,会通过调用另一个类加载器deferTo
的loadClass
方法来加载。这样做应该是为了避免自定义的类加载器干扰到系统类的加载 - 如果仍然没有加载成功,方法会进行第三次尝试,处理一个特殊请求:若类名以
$$BCEL$$
开头则调用createClass()
创建一个类,这个方法将 BCEL 字节码转换成 JavaClass 对象 - 不包含以
$$BCEL$$
则通过一个 repository 加载类 - 如果上面两步成功获取到了 Class 对象(无论是创建的还是从 repository 加载的),就会调用
defineClass()
- 上述步骤都失败则使用默认类加载器
Class.forName()
为了创建用于被加载的 BCEL 字节码,可以用 BCEL 提供的两个类 Repository
Utility
Repository.lookup()
可以加载一个类,解析为 JavaClass 对象Utility.encode()
将 JavaClass 对象转换成 BCEL 字节码
com.sun.org.apache.bcel.internal.classfile.JavaClass
是 BCEL 库中专门用于表示和操作 Java 类文件(也就是字节码)的类,它包含了比 java.lang.Class 更底层的信息
public class CustomClass {
static {
System.out.println("CustomClass loaded");
}
}
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
class BCELExample {
public static void main(String []args) throws Exception {
JavaClass cls = Repository.lookupClass(CustomClass.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
}
}
自定义类加载器
在某些特殊情况下,JDK 提供的 ClassLoader 可能无法满足需求,这时就需要自定义 ClassLoader。CTF 中如果存在任意代码执行的情况,也可以通过自定义类加载器来绕过一些限制
如何自定义 ClassLoader:
- 继承
java.lang.ClassLoader
类 - 重写
findClass(String name)
方法 在这个方法中实现从特定来源加载类的字节码的逻辑 - (可选)重写
loadClass(String name)
方法 如果需要改变默认的双亲委派模型,可以重写这个方法 - (可选)重写
findResource()
或getResources()
方法 如果需要从自定义位置加载资源文件
示例:
从 base64 编码的字节码加载类的自定义 ClassLoader:
package com.kkayu;
import java.util.Base64;
class Test {
public static class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
byte[] bytecode = Base64.getDecoder().decode("yv66vgAAADQAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTEN1c3RvbUNsYXNzOwEACDxjbGluaXQ+AQAKU291cmNlRmlsZQEAEEN1c3RvbUNsYXNzLmphdmEMAAcACAcAGQwAGgAbAQASQ3VzdG9tQ2xhc3MgbG9hZGVkBwAcDAAdAB4BAAtDdXN0b21DbGFzcwEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAIAAQAHAAgAAQAJAAAALwABAAEAAAAFKrcAAbEAAAACAAoAAAAGAAEAAAABAAsAAAAMAAEAAAAFAAwADQAAAAgADgAIAAEACQAAACUAAgAAAAAACbIAAhIDtgAEsQAAAAEACgAAAAoAAgAAAAMACAAEAAEADwAAAAIAEA==");
return defineClass(name, bytecode, 0, bytecode.length);
}
}
public static void main(String[] args) throws Exception {
CustomClassLoader loader = new CustomClassLoader();
Class<?> customClass = loader.findClass("CustomClass");
Object instance = customClass.newInstance();
}
}
另一种使用匿名内部类的写法:
package com.kkayu;
import java.util.Base64;
class Test {
public static void main(String[] args) throws Exception {
ClassLoader CustomClassLoader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytecode = Base64.getDecoder().decode("yv66vgAAADQAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTEN1c3RvbUNsYXNzOwEACDxjbGluaXQ+AQAKU291cmNlRmlsZQEAEEN1c3RvbUNsYXNzLmphdmEMAAcACAcAGQwAGgAbAQASQ3VzdG9tQ2xhc3MgbG9hZGVkBwAcDAAdAB4BAAtDdXN0b21DbGFzcwEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAIAAQAHAAgAAQAJAAAALwABAAEAAAAFKrcAAbEAAAACAAoAAAAGAAEAAAABAAsAAAAMAAEAAAAFAAwADQAAAAgADgAIAAEACQAAACUAAgAAAAAACbIAAhIDtgAEsQAAAAEACgAAAAoAAgAAAAMACAAEAAEADwAAAAIAEA==");
return this.defineClass(name, bytecode, 0, bytecode.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
};
CustomClassLoader loader = new CustomClassLoader();
Class<?> customClass = loader.findClass("CustomClass");
Object instance = customClass.newInstance();
}
}
此外,除了直接调用 findClass 加载类,也可以通过 Class.forName()
加载
Class clazz = Class.forName("CustomClass", true, CustomClassLoader);
Object instance = clazz.newInstance();
参考:
- 《Java 安全漫谈》
- ClassLoader in Java - GeeksforGeeks