java动态加载字节码
一、前言
在前面分析CC1和CC6利用链的时候,都是通过transform之间之间执行命令,但是在cc3中是通过动态类加载来实现自动执行恶意类的代码。然后这里在对cc3利用链分析前,先对java动态加载字节码进行一个学习。
二、java字节码
java字节码是java程序源代码经过编译器(javac)编译生成的中间代码,严格来说,它并不是本地机器代码,而是一种与平台无关的低级指令集。这些指令由java虚拟机(JVM)解释执行,使得java程序可以一次编写,随处执行。使得上层开发者只需将自己的代码编译一次,就可以运行在不同平台的JVM虚拟机中。
我们写的java(.java)程序通过编译器编译成的就是字节码文件(.class)。
1 | public class helloworld { |
通过编译器进行编译:
1 | javac 文件名.java |
我们这里通过010打开编译器生成的class文件就可以看到生成的字节码了。
可以看到在我们生成的字节码中包含了常量,访问标志,类信息,字段信息,方法信息,方法代码,以及字节码指令等等这些内容共同构成了java字节码结构,java虚拟机在运行时通过解释这些字节码指令来执行java程序。
具体的关于字节码的每一块的解释可以看看这篇文章:
https://www.jianshu.com/p/fa53b4169df9
下图引自P神java安全漫谈
三、ClassLoader(类加载器)
在上面我们说可以java虚拟机通过解释字节码来执行java程序
但是我们想要通过解释器去解释字节码执行java程序要先将字节码加载到内存中,那么在java虚拟机中将字节码加载到内存中就是通过类加载器。这个JVM的重要组件来实现的。当classloader将字节码加载到内存中,会创建相应的Class对象,然后解释器逐条解释执行这些字节码指令,然后执行对应的操作,并根据指令的结果继续执行下一条指令。解释器的作用是将字节码翻译成实际的操作,实现了 Java 代码在不同平台上的执行。
但是系统程序在启动时,不会一次性加载所有的程序要使用的Class文件到内存中,而是根据程序的需要,通过类加载机制动态将程序要使用的Class文件加载到内存中。只有当class文件杯加载到内存中,才能够被调用。这个机制其实就是类加载机制,也就是classloader(classloader在这里既指类加载器也指类加载机制)。
classloader类的核心方法:
loadClass
(加载指定的Java类)findClass
(查找指定的Java类)findLoadedClass
(查找JVM已经加载过的类)defineClass
(定义一个Java类)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的加载器分类:
然后我们这里了解一下类加载的双亲委派
下面我们通过一张图来介绍双亲委派模型:
双亲委派机制
分为委托阶段和派发阶段:
委托阶段:
当一个类被加载的时候首先会先判断自己是否已经加载,如果加载了之间返回相应的对象,如果没有被加载,则委托给父类加载器。父类加载器同样也是判断是否加载,同样未加载的话,委托给其父类加载器,直至到达顶层的类加载器引导类加载器(Bootstrap ClassLoade),相应的加载了就返回相应对象,如果直到引导层加载器都未加载成功,说明类未加载,这时就会进入派发阶段,查找并加载类。
派发阶段:
当到达引导类加载器,bootstrapClassLoader 会去对应的目录下(%JAVA_HOME%jre/lib/
)搜索该类,找到了就加载类,未能找到就派发给子类加载器进行加载,子类执行的也是执行同样的操作,搜索这个类,有的话加载类,没有继续派发给子类加载器。
根据这个模型我们可以看到最终会到达自定义加载器,如果自定义加载器也未能加载成功就会抛出ClassNotFoundException
异常并退出。
综合来说就是,当我们加载一个类的时候会先判断自己是否加载,加载的话就直接返回,没加载就调用父类加载器,直到引导类加载器都没有加载成功,在调用子类加载器进行加载,直到调用到自定义加载器。
我们这里结合loaderclass代码进行具体分析一下:
1 | protected Class<?> loadClass(String name, boolean resolve) |
首先通过findLoadedClass(name)方法检查类是否已经加载了,如果加载了就直接返回,不会重复加载,没有加载就做加载处理,
1 | if (c == null) { |
如果类未被加载,首先优先尝试使用父类加载器即调用 parent.loadClass(name, false)(这里其实也就是双亲委派加载机制);
方法parent是当前加载的父类加载器。同样父类加载器在加载类时也会按同样的方法进行。
如果父类加载器加载成功,返回相应的class对象 c。
如果未加载成功,继续调用父类的父类加载器直到尝试调用引导类加载器,这里即调用findBootstrapClassOrNull(name)
,这里关于引导类加载器我们上面有介绍。同样的加载成功返回对象c,如果引导类加载器也未能加载成功,说明类既不在父类加载器中,也不在引导类加载器中,表面类还未加载。
则会调用findClass(name)
方法
我们这里跟进到findclass方法:
我们可以看到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,基础数据类型的包装类、基于异常数据的包装类等等这些。
类加载的过程
上面我们了解了类加载的两种方式,下面我们具体分析一下类加载的过程:
- 加载(Loading): 加载是类加载的第一个阶段,它是将类的字节码文件(通常是以
.class
后缀的文件)从磁盘或网络加载到内存中的过程。加载过程由类加载器(ClassLoader)来完成。类加载器根据类的全限定名(包括包名和类名)查找类的字节码文件,然后读取字节码文件,并创建对应的Class
对象。 - 连接(Linking): 连接是类加载的第二个阶段,它包括三个子阶段:验证、准备和解析。
- 验证(Verification): 在验证阶段,JVM 将对加载的字节码进行验证,以确保字节码是合法、符合规范的。验证阶段主要包括类型检查、字节码验证、符号引用验证等,用于确保字节码的正确性和安全性。
- 准备(Preparation): 在准备阶段,JVM 为类的静态变量(类变量)分配内存,并设置默认初始值。这些静态变量会在类加载完成后被初始化为指定的初始值。注意,实例变量在这个阶段并不会被赋予初值,它们会在对象实例化时进行初始化。
- 解析(Resolution): 在解析阶段,JVM 将符号引用替换为直接引用。符号引用是一种在字节码中使用的符号来表示目标类或方法的引用,而直接引用是指向目标的真实指针或句柄。解析过程将符号引用转换为直接引用,以便在后续的执行中能够直接定位目标类或方法。
- 初始化(Initialization): 初始化是类加载的最后一个阶段,在这个阶段,JVM 执行类的初始化代码,为静态变量赋予正确的初始值,并执行类中定义的静态初始化块。类的初始化是在类加载的最后阶段进行的,只有在真正使用类时才会触发初始化,例如创建类的实例、调用类的静态方法、访问类的静态变量等。初始化阶段可以包括复杂的逻辑和代码,这取决于类的定义和开发人员编写的初始化代码。
四、动态加载字节码
上面我们对classloader进行了学习,然后这里我们正式开始学习java如何动态加载字节码
URLClassLoader加载远程class文件
可以看到URLclassloader是继承 SecureClassLoader类的
而这里的 SecureClassLoader是继承了CLassloader类的,而且这个URLclassloader是我们上面介绍的应用程序加载器(appclassloader)的父类
我们在上面双亲委派机制中说过,会先父类查询父类是否加载,直到引导下型加载器都未加载,开始从引导型加载器开始搜索类进行加载,引导型没有找到,子类进行查找加载,直到自定义加载器。
但是这个搜索是怎么搜索的呢?
以下内容引自P神java安全漫谈
正常情况下,Java会根据配置项 sun.boot.class.path
和 java.class.path
中列举到的基础路径(这
些路径是经过处理后的 java.net.URL
类)来寻找.class
文件来加载,而这个基础路径有分为三种情况:
- URL未以斜杠
/
结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件 - URL以斜杠
/
结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件 - URL以斜杠
/
结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类
我们这里通过http协议进行测试:
1 | import java.net.URL; |
编译一个class文件放在服务器所在目录
1 | public class Hello { |
我这里之间在class文件所在开一个服务
可以看到成功加载到远程服务器上面的class文件,并执行了字节码输出了我们定义的helloworld
ClassLoader#defineClass() 加载字节码
我们这里在回顾一下双亲委派的实现代码
1 | Class<?> c = findLoadedClass(name); |
我们在双亲委派中学习了,在加载一个类时首先会先调用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 | protected final Class<?> defineClass(String name, byte[] b, int off, int len) |
可以看到defineclass是一个受保护的方法,我们不能够直接调用,所以我们如果要外部调用的话,这里要通过反射来获取这个defineclass方法。
接收三个参数,一个字符方法名,一个字节数组,一个偏移量一个长度
示例代码:
1 | import java.lang.reflect.Method; |
这里的code就是字节码
ClassLoader.getSystemClassLoader()获取系统类加载器实例,返回一个classloader对象
然后调用sefineclass方法对字节码进行处理转换为java类
运行结果:
这里要注意的是defineclass被调用的时候,类对象不会被初始化,只有显式调用构造函数,初始化代码才会被执行,而且即使我们的代码放到类的静态代码块中也无法直接被调用到,如果我们要使用 defineClass 在目标机器上执行任意代码,需要想办法调用构造函数。
利用TemplatesImpl加载字节码
上面我们分析了defindclass加载字节码,但是我们defineclass这个方法是受保护的方法,不能够直接被调用,所以这里我们不能够直接调用。
那么我们要想使用这个方法执行任意代码就要向上找调用:
虽然大多数开发者不会直接使用defineclass方法,但是在java的一些底层方法中还是有调用了defineclass的
我们这里利用的是TemplatesImp
l
在TemplatesImp
l的TransletClassLoader类中重写了defineclass方法
跟进到
1 | static final class TransletClassLoader extends ClassLoader { |
而且这里并没有显式的声明其定义域。在java中如果一个方法没有显式的声明作用域,那么其作用域为default。那么这里的defineclass方法就由父类的protected变成了一个default类型的方法。
那么我们就要在TemplatesImp
l中看一下哪里调用了defineclass
根据查找用法我们可以在defineTransletClasses()中找到调用
但是这个是一个私有方法,不能直接调用,继续向上找调用:
getTransletInstance()同样是一个私有方法,继续向上找调用:
在newTransformer中可以找到对getTransletInstance()的调用,而且newTransformer是一个公有方法,那么我们这里外部可以直接调用,那么利用链就到这里结束了。
这里给出利用链:
1 | TemplatesImpl#newTransformer() -> |
但是这只是利用链,我们要想要利用这条链,就要解决细节部分,写出POC实现利用:
我们继续回到newTransformer()的getTransletInstance()
1 | transformer = new TransformerImpl(getTransletInstance(), _outputProperties, |
根据代码我们可以看到当_name
不为空,_class
为空时才能够调用defineTransletClasses()方法
但是默认情况下这两个参数都为空
所以我们这里想要调用defineTransletClasses()方法就要修改_name不为空
然后我们在类中可以找到它的构造器:
1 | public TemplatesImpl() { } |
那么我们这里就可以通过反射去修改属性值:
即:
1 | Field field = obj.getClass().getDeclaredField(fieldName);//获取class对象后获取定名称 fieldName 的属性的 Field 对象 |
这里由于要修改的很多,所以我们这里直接定义一个方法来简化我们的代码:
1 | public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{ |
那么我们的POC为:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
继续跟进到defineTransletClasses():
我们这里先看一下我们要调用的defineClass()方法:
1 | try { |
我们在调用defineclass
方法时还传入了一个字节数组_bytecodes[i]
那么结合前面这里这个字节数组就是我们要传入的字节码
那么其实这个就是实现了遍历字节数组取出当中的字节码,然后传入defineclass进行处理
但是我们不可能调用defineTransletClasses()
就能够直接调用到defineClass
我们这里分析一下上面的代码逻辑:
1 | if (_bytecodes == null) { |
首先一个是对_bytecodes
判断是否为空,这里的_bytecodes
其实就是我们的字节码数组,那么我们这里把我们的字节码赋值给_bytecodes
就可以使其不为空
我们这里还是通过反射去修改属性值:
1 | Field filed = templates.getClass().getDeclaredField("_bytecodes"); |
1 | TransletClassLoader loader = (TransletClassLoader) |
或者是直接加载远程或者本地class文件:
1 | byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\CC\\target\\classes\\EvilTemplatesImpl.class")); |
其实效果都是一样的。
然后是一个run方法
1 | TransletClassLoader loader = (TransletClassLoader) |
这里有一个_tfactory
,因为用到了这个参数所以我们要给这个参数赋值,不然这里就会报错
transient
关键字是 Java 中的一个修饰符,用于标记变量,表示该变量在对象的序列化过程中不会被持久化,即不会被写入到字节流中,从而在反序列化后该变量的值会被重新初始化。
所以说我们这里给这个参数去赋值时没有意义的,因为当他在被反序列化的时候又被重新初始化了。
那我们直接跟进到readobject中去看一下:
可以看到在反序列化的时候是会给这个参数赋值的
但是我们这里要使用这个参数
所以我们这里可以通过反射随便给它赋值,因为无论我们赋什么值,这里在反序列化的时候的值是不会改变的
然后我们这里为了方便直接赋值new TransformerFactoryImpl
,方便我们后面调试,因为我们利用链到这里还不会进行反序列化,所以我们这里先赋值给它。
1 | Field filed = templates.getClass().getDeclaredField("_tfactory"); |
然后我们这里为了简化代码,直接把反射修改属性值这一部分代码定义一个函数:
1 | public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{ |
那么到这里我们的POC可以编写为:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
那么正常情况下,我们这里应该已经可以正常进行命令执行
那么我们这里编写一个命令执行的恶意代码类:
1 | public class Test { |
然后编译一下把字节码base64编码一下:
1 | public class bianma { |
我们直接把我们的POC运行一下:
但是并没有执行命令
然后是报了一个空指针错误在defineTranslectClasses里面
但是这个不太好找
然后我们这里打断点跟进调试一下:
跟进
然后我们可以看到这里类加载是已经成功了
然后我们继续跟进
然后我们可以看到就是在这个if判断里面报了一个空指针错误
然后这个if就是检查我们传入字节码是不是ABSTRACT_TRANSLET
的子类
然后是的话会对_transletIndex
进行一个赋值,不是的话,就会跳转到我们报空指针错误的地方
那我们这里解决办法有两个:
1、给_auxCLasses赋值,解决这个空指针报错
2、使if判断为真也就是我们传入的字节码为ABSTRACT_TRANSLET
的子类
但是我们往下看还有一个if判断:
1 | if (_transletIndex < 0) { |
这里也就是_transletIndex
值小于0就直接抛出异常
而且我们现在的_transletIndex
值是-1
所以我们给_auxCLasses赋值不可行,因为这样无法过第二个if,仍然会报错
所以我们这里要使我们传入的字节码为ABSTRACT_TRANSLET
的子类
跟进到这个类里面
可以看到这个类是一个抽象类,那他的子类就要实现它的抽象方法
所以我们构造的恶意类就要实现这个transform抽象方法
那我们这里重新构造一个恶意类
1 | public class HelloTemplatesImpl extends AbstractTranslet { |
那么我们的POC就可以构造为:
1 | public class blog { |
可以看到已经成功运行了。但是这里我们并没有进行newInstance(),但是依然弹出了计算器,
那我们这里就打断点整体分析一下:
可以看到这里是显调用了run方法创建一个TransletClassLoader
对象对象
继续跟进:
由于我们这里只传入了一个字节数组,所以这里的_bytecodes.length的值就为1,赋值给classCount
1 | _class = new Class[classCount]; |
继续跟进:
可以看到这里第一轮循环,处理了我们传入的字节数组,然后经过if判断将i的值0赋值给了_transletIndex
然后就步出回到了getTransletInstance()
可以看到这里调用了_class[0]的newInstance()方法,弹出计算器
所以这就是我们为什么不用再newinstace的原因了。
利用链:
1 | TemplatesImpl#newTransformer() -> |
利用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 进行许可。