# Introduction
又是回过头来填坑的一篇,刚开始开 java 这个坑的时候就想着把这一块的内容记录一下。后来似乎因为一些事情忘记了,主要也是前面留下的坑太多导致的。这里我也是参考着大师傅的文章学习一下这块内容,本篇中所涉及的代码都在 https://github.com/clown-q/Clown_java 中
参考(照搬照抄)文章:[ClassLoader・攻击 Java Web 应用 -Java Web 安全] (javasec.org)
# ClassLoader
# 是什么?
首先就是这样一个问题,什么是 ClassLoader?我们编写的 java 文件都是以.java 为后缀的文件,编译器会将我们编写的 java 文件编译为.class 后缀结尾的文件
上图中就是在 idea 中我们编写的 java 文件和经过编译器编译后的 class 文件所在位置和对应的关系。而我们编译好的 class 文件想要运行还需要一个小玩意(JVM),对于 jvm 相关的内容这里也不在这里赘述,可以翻一下我之前填的坑分类:JavaJVM | Clown の Blog = (xcu.icu)。一个 java 类想要运行必须要经过 JVM 的加载后才能正常的运行,而 Classloader 的作用就是将 class 文件加载到 JVM 中。
Classloader 也不是什么很神秘的东西,在上图中也可以看到它实际上就是一个抽象类。其中的方法很多,但是我们主要需要关注的只有下面 4 个方法
- loadClass(加载指定的 Java 类)
- findClass(查找指定的 Java 类)
- findLoadedClass(查找 JVM 已经加载过的类)
- defineClass(定义一个 Java 类)
# 类加载器
说到类加载不得不提的就是类加载器,主要有在 JVM 中最顶层的 Bootstrap Classloader(引导类加载器)、Extension Classloader(扩展类加载器)、App Classloader(系统类加载器)、User Classloader(用户自定义加载器)这四种类加载器。他们是以双亲委派这种机制在工作
protected Class<?> loadClass(String name, boolean resolve) | |
throws ClassNotFoundException | |
{ | |
synchronized (getClassLoadingLock(name)) { | |
// 首先检查类是否已经被加载 | |
Class<?> c = findLoadedClass(name); | |
if (c == null) { | |
long t0 = System.nanoTime(); | |
try { | |
if (parent != null) { | |
// 父类不是 BootstrapClassloader 的时候会调用其父类进行加载 | |
c = parent.loadClass(name, false); | |
} else { | |
c = findBootstrapClassOrNull(name); | |
} | |
} catch (ClassNotFoundException e) { | |
// 如果找不到类,则抛出 ClassNotFoundException | |
// 从非空父类加载器 | |
} | |
if (c == null) { | |
// 如果仍未找到,则按顺序调用 findClass | |
long t1 = System.nanoTime(); | |
c = findClass(name);// 这里如果没有重写这个 findClass 方法会直接抛出一个异常 | |
// 这是定义类装入器;记录统计数据 | |
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); | |
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); | |
sun.misc.PerfCounter.getFindClasses().increment(); | |
} | |
} | |
if (resolve) { | |
resolveClass(c); | |
} | |
return c; | |
} | |
} |
上面这段代码实际上已经注释的很明白了,这里放一张我自己做的图来加深一下理解
值得注意的是。Classloader 的类加载器也是 AppClassLoader
# 自定义 ClassLoader
其实这里也没有那么高大上,实际上它的本质就是方法重写。Classloader 是所有类加载器的父类,我们可以通过重写其中的方法来加载我们的类。我热衷于弹一个计算器,绝对不是因为我懒得写新的。
package ClassLoaderStudy; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: ClassLoaderStudy | |
* @Author: Clown | |
* @CreateTime: 2023-10-29 18:21 | |
*/ | |
public class UserClassloaderTest extends ClassLoader{ | |
public String ClassName = "B"; | |
public byte[] bytes = new byte[]{-54,-2,-70,-66,0,0,0,52,0,40,10,0,9,0,24,10,0,25,0,26,8,0,27,10,0,25,0,28,7,0,29, | |
7,0,30,10,0,6,0,31,7,0,32,7,0,33,1,0,6,60,105,110,105,116,62,1,0,3,40,41,86,1,0,4,67,111,100,101,1,0,15, | |
76,105,110,101,78,117,109,98,101,114,84,97,98,108,101,1,0,18,76,111,99,97,108,86,97,114,105,97,98,108,101, | |
84,97,98,108,101,1,0,4,116,104,105,115,1,0,3,76,66,59,1,0,8,60,99,108,105,110,105,116,62,1,0,1,101,1,0,21, | |
76,106,97,118,97,47,105,111,47,73,79,69,120,99,101,112,116,105,111,110,59,1,0,13,83,116,97,99,107,77,97,112, | |
84,97,98,108,101,7,0,29,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,6,66,46,106,97,118,97,12,0,10,0,11,7, | |
0,34,12,0,35,0,36,1,0,4,99,97,108,99,12,0,37,0,38,1,0,19,106,97,118,97,47,105,111,47,73,79,69,120,99,101, | |
112,116,105,111,110,1,0,26,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,69,120,99,101,112, | |
116,105,111,110,12,0,10,0,39,1,0,1,66,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,17, | |
106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,1,0,10,103,101,116,82,117,110,116,105,109,101, | |
1,0,21,40,41,76,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,59,1,0,4,101,120,101,99,1,0,39, | |
40,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,76,106,97,118,97,47,108,97,110,103,47, | |
80,114,111,99,101,115,115,59,1,0,24,40,76,106,97,118,97,47,108,97,110,103,47,84,104,114,111,119,97,98,108,101, | |
59,41,86,0,33,0,8,0,9,0,0,0,0,0,2,0,1,0,10,0,11,0,1,0,12,0,0,0,47,0,1,0,1,0,0,0,5,42,-73,0,1,-79,0,0,0,2,0,13, | |
0,0,0,6,0,1,0,0,0,9,0,14,0,0,0,12,0,1,0,0,0,5,0,15,0,16,0,0,0,8,0,17,0,11,0,1,0,12,0,0,0,102,0,3,0,1,0,0,0,23, | |
-72,0,2,18,3,-74,0,4,87,-89,0,13,75,-69,0,6,89,42,-73,0,7,-65,-79,0,1,0,0,0,9,0,12,0,5,0,3,0,13,0,0,0,22,0,5, | |
0,0,0,12,0,9,0,15,0,12,0,13,0,13,0,14,0,22,0,16,0,14,0,0,0,12,0,1,0,13,0,9,0,18,0,19,0,0,0,20,0,0,0,7,0,2,76, | |
7,0,21,9,0,1,0,22,0,0,0,2,0,23 | |
}; | |
@Override | |
protected Class<?> findClass(String name) throws ClassNotFoundException { | |
System.out.println("调用了自定义的Classloader"); | |
if (name.equals(ClassName)) { | |
System.out.println(ClassName); | |
return defineClass(ClassName, bytes, 0, bytes.length); | |
} | |
return super.findClass(name); | |
} | |
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { | |
UserClassloaderTest userClassloaderTest = new UserClassloaderTest(); | |
Class<?> aClass = userClassloaderTest.loadClass("B"); | |
Object newInstance = aClass.newInstance(); | |
} | |
} |
这里 byte 中的内容是下面这段代码编译后的 class 文件得到的
import java.io.IOException; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: PACKAGE_NAME | |
* @Author: Clown | |
* @CreateTime: 2023-10-11 19:52 | |
*/ | |
public class B { | |
static { | |
try { | |
Runtime.getRuntime().exec("calc"); | |
} catch (IOException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} |
然后通过下面的代码来得到 Byte 中的内容
public class test{ | |
public static void main(String[] args) throws Exception{ | |
String filePath = "E:\\study_java\\target\\classes\\B.class"; | |
try (FileInputStream fis = new FileInputStream(filePath)) { | |
byte[] data = new byte[fis.available()]; // 创建一个字节数组,大小等于文件大小 | |
fis.read(data); // 读取文件内容到字节数组 | |
// 打印字节数据 | |
for (byte b : data) { | |
System.out.print(b + ","); | |
} | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} |
这里在测试上面的代码的时候要注意将原本的 B 类注释掉
# URLClassLoader
UrlClassLoader 其实在前面 CC3 的分析中已经有提到也用过 ,但是这并不影响我再水一遍,其中默认的类加载器的继承关系
它是 AppClassLoader 的父类实际上也是继承了 ClassLoader,这个 URLClassLoader 实际上所有的 jar 包都是通过这个加载器来加载的,使用 file 协议这种去加载的。当然在前面的 CC3 中就已经器加载远程的文件
package ClassLoaderStudy; | |
import java.net.URL; | |
import java.net.URLClassLoader; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: ClassLoaderStudy | |
* @Author: Clown | |
* @CreateTime: 2023-10-31 13:56 | |
*/ | |
public class URLClassLoaderTest { | |
public static void main(String[] args) throws Exception{ | |
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("Http://127.0.0.1:8000/")}); | |
Class<?> c = urlClassLoader.loadClass("B"); | |
c.newInstance(); | |
} | |
} |
这里和上面一样,也是需要将项目中的这个文件给注释掉,不然这里还是会先加载本地的
# 类加载隔离
创建类加载的时候是可以指定该加载类的父类加载器的,比如说上面的 URLClassLoader 这个类中
他是有参数可以指定其父类加载器的
package ClassLoaderStudy; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: ClassLoaderStudy | |
* @Author: Clown | |
* @CreateTime: 2023-10-31 14:33 | |
*/ | |
public class ClassLoaderIsolate { | |
public static String ClassName = "B"; | |
public static byte[] bytes = new byte[]{-54,-2,-70,-66,0,0,0,52,0,40,10,0,9,0,24,10,0,25,0,26,8,0,27,10,0,25,0,28,7,0,29, | |
7,0,30,10,0,6,0,31,7,0,32,7,0,33,1,0,6,60,105,110,105,116,62,1,0,3,40,41,86,1,0,4,67,111,100,101,1,0,15, | |
76,105,110,101,78,117,109,98,101,114,84,97,98,108,101,1,0,18,76,111,99,97,108,86,97,114,105,97,98,108,101, | |
84,97,98,108,101,1,0,4,116,104,105,115,1,0,3,76,66,59,1,0,8,60,99,108,105,110,105,116,62,1,0,1,101,1,0,21, | |
76,106,97,118,97,47,105,111,47,73,79,69,120,99,101,112,116,105,111,110,59,1,0,13,83,116,97,99,107,77,97,112, | |
84,97,98,108,101,7,0,29,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,6,66,46,106,97,118,97,12,0,10,0,11,7, | |
0,34,12,0,35,0,36,1,0,4,99,97,108,99,12,0,37,0,38,1,0,19,106,97,118,97,47,105,111,47,73,79,69,120,99,101, | |
112,116,105,111,110,1,0,26,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,69,120,99,101,112, | |
116,105,111,110,12,0,10,0,39,1,0,1,66,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,17, | |
106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,1,0,10,103,101,116,82,117,110,116,105,109,101, | |
1,0,21,40,41,76,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,59,1,0,4,101,120,101,99,1,0,39, | |
40,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,76,106,97,118,97,47,108,97,110,103,47, | |
80,114,111,99,101,115,115,59,1,0,24,40,76,106,97,118,97,47,108,97,110,103,47,84,104,114,111,119,97,98,108,101, | |
59,41,86,0,33,0,8,0,9,0,0,0,0,0,2,0,1,0,10,0,11,0,1,0,12,0,0,0,47,0,1,0,1,0,0,0,5,42,-73,0,1,-79,0,0,0,2,0,13, | |
0,0,0,6,0,1,0,0,0,9,0,14,0,0,0,12,0,1,0,0,0,5,0,15,0,16,0,0,0,8,0,17,0,11,0,1,0,12,0,0,0,102,0,3,0,1,0,0,0,23, | |
-72,0,2,18,3,-74,0,4,87,-89,0,13,75,-69,0,6,89,42,-73,0,7,-65,-79,0,1,0,0,0,9,0,12,0,5,0,3,0,13,0,0,0,22,0,5, | |
0,0,0,12,0,9,0,15,0,12,0,13,0,13,0,14,0,22,0,16,0,14,0,0,0,12,0,1,0,13,0,9,0,18,0,19,0,0,0,20,0,0,0,7,0,2,76, | |
7,0,21,9,0,1,0,22,0,0,0,2,0,23 | |
}; | |
public static class ClassLoaderA extends ClassLoader { | |
public ClassLoaderA(ClassLoader parent) { | |
super(parent); | |
} | |
{ | |
// 加载类字节码 | |
defineClass(ClassName, bytes, 0, bytes.length); | |
} | |
} | |
public static class ClassLoaderB extends ClassLoader { | |
public ClassLoaderB(ClassLoader parent) { | |
super(parent); | |
} | |
{ | |
// 加载类字节码 | |
defineClass(ClassName, bytes, 0, bytes.length); | |
} | |
} | |
public static void main(String[] args) throws ClassNotFoundException { | |
ClassLoader classLoader = ClassLoader.getSystemClassLoader(); | |
ClassLoaderA classLoaderA = new ClassLoaderA(classLoader); | |
ClassLoaderB classLoaderB = new ClassLoaderB(classLoader); | |
Class classa = Class.forName(ClassName,true,classLoaderA); | |
Class classaa = Class.forName(ClassName,true,classLoaderA); | |
Class classb = Class.forName(ClassName,true,classLoaderB); | |
System.out.println("classa == classaa"+"======"+(classa == classaa)); | |
System.out.println("classa == classb" +"======"+(classa == classb)); | |
// System.out.println(classa.getName()); | |
} | |
} |
ClassLoader A 和 ClassLoader B 可以加载相同类名的类,但是 ClassLoader A 中的 Class A 和 ClassLoader B 中的 Class A 是完全不同的对象,两者之间调用只能通过反射
# BCEL ClassLoader
这个 BCEL 的 ClassLoader 实际上在前面记录 FastJson 反序列化漏洞的时候就已经了解过连接:
FastJson 漏洞分析 - java 安全 | Clown の Blog = (xcu.icu)
BCEL( Apache Commons BCEL™
)是一个用于分析、创建和操纵 Java 类文件的工具库,Oracle JDK 引用了 BCEL 库,不过修改了原包名 org.apache.bcel.util.ClassLoader 为 com.sun.org.apache.bcel.internal.util.ClassLoader,也就是说这里实际上是不需要依赖的
在其 ClassLoader 方法中这里会有一个判断,如果类名中存在 $$BCEL$$ 就会调用这个 createClass 方法
这里会将这个类名当做类加载出来,简单来说只需要类名就可以得到一个类,又是 sun 包下的内容
这个特性在 BCEL6.0 以下支持
# Xalan ClassLoader
Oracle JDK 默认也引用了 Xalan,同时修改了原包名 org.apache.xalan.xsltc.trax.TemplatesImpl 为 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,没错就是我们常用的这个 TemplatesImpl 类。TemplatesImpl 可以传入类字节码并初始化。这里也可以看到,它与上面的 BCEL 需要进行一次加密是不一样的,这里值需要直接将字节码传入即可。当然在 FastJson 漏洞分析 - java 安全 | Clown の Blog = (xcu.icu) 我也是有记录到的
在 FastJson 里利用实际上就是在 com.sun.org.apache.xalan.internal.xsltc.trax 类中的 getTransletInstance 方法中
这里有一个 newInstance 方法可以实实例化对象,这里看一下 _class [_transletIndex] 是不是可控的
这里可以看到其实在当前类的 defineTransletClasses 方法中,这里可以看到,这里的_transletIndex 是可以根据_bytecodes 改变的
而在本类下,还有对应的 set 和 get 方法,很显然是可控的,这里我们就需要看一下 defineTransletClasses 方法
首先这里会判断_bytecodes 是否为空,这里可控,所以这里我们可以传入一个恶意的类
_bytecodes 不为空,运行到这里,此时,需要满足_tfactory 变量不为 null,否则导致程序异常退出。
这个成员属性没有对应的 set 和 get 方法,这里需要指定 Feature.SupportNonPublicField 参数来为_tfactory 赋值
把_bytecodes 数组中的值循环取出,使用 loader.defineClass 方法从字节码转化为 Class 对象,随后后赋值给_class [i],如果循环到 main class,就会将_transletIndex 变量值赋值为_bytecodes 数组中的下标值
在回到这个入口类,当前想要执行到 newInstance 方法,只需要_name 不为 null 即可
它有对应的 set 方法,到这里条件就都满足了
利用条件十分之苛刻