Java动态类加载
之前我们学习了ClassLoader,这里我们继续学习相关的类加载器。
URLClassLoader
URLClassLoader
可以指定一串路径,然后再些路径下面寻找将要加载的类。这个类加载器如果没有指定的话父类加载器也是AppClassLoader
。Java会根据配置项sun.boot.class.path
和java.class.path
中列举的基础路径来寻找class
文件,基础路径分为如下三种情况。
- URL不是以
/
结尾,则会认为这是一个Jar文件,使用JarLoader来寻找类,即在Jar包中寻找.class
文件 - URL以斜杠
/
结尾,且使用file协议,则会使用FileLoader
来寻找类,即本地文件系统中寻找.class
文件 - URL以斜杠
/
结尾,但没有使用file协议的,则会默认最基础的Loader寻找类
开发中常用得是前两种,那如果需要使用Loader
寻找类的时候,就需要用到非file
协议,最常见的是http协议,我们可以通过这种方式直接加载远端的class文件,所以如果我们控制了目标Java ClassLoader
的基础路径为一个http服务器,即可进行RCE。
本地磁盘class文件调用
这里我们首先尝试一下URLClassLoader的从本地寻找类,这边我们编写一个测试文件,该文件的执行结果是调用计算器并输出hello
。
将该文件编译成class文件
编写ClassLoader测试类,利用URLClassLoader方式获取D本地磁盘中的class文件
执行结果:
网络传输class文件调用
这里我们测试一下HTTP协议,尝试从远程HTTP服务器上加载class
文件
这里我们将之前生成的class文件放到云服务器上,然后使用python生成一个HTTP服务,当然本地开启HTTP服务也可以进行测试。
然后修改我们ClassLoader测试代码
这里有一个包名的问题,如果我们的包名为com.alexsel
,则我们需要将文件放在com
文件夹下的alexsel
文件夹下,然后使用如下代码获取
这个代码的请求连接为如下图
如果没有包名的情况下,直接将文件放在根目录下,使用如下代码进行获取
当类文件没有包名,但是我们将其放在com/alexsel
目录下,虽然会请求成功,但是依然会报错
类加载隔离
类隔离的原理就是让 每个模块使用独立的类加载器来加载 ,这样不同模块之间的依赖就不会互相影响,使得不同版本类间隔离,避免了使用冲突问题。当两个不同的类加载器,加载同一个类即可实现 相互隔离 ,但是要注意 双亲委派模型 ,不要让父类加载器加载到你要加载的类。
创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两则必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。
跨类加载器加载
RASP和IAST经常会用到跨类加载器加载类的情况,因为RASP/IAST会在任意可能存在安全风险的类中插入检测代码,因此必须得保证RASP/IAST的类能够被插入的类所使用的类加载正确加载,否则就会出现ClassNotFoundException,除此之外,跨类加载器调用类方法时需要特别注意一个基本原则:ClassLoader A和ClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class A和ClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射
。
JSP自定类加载后门
我们熟知的冰蝎
编写的JSP后门利用的就是自定义类加载实现的,冰蝎的客户端会将待执行的命令或代码片段通过动态编译成类字节码并加密后传到冰蝎的JSP后门,后门会经过AES解密得到一个随机类名的类字节码,然后调用自定义的类加载器加载,最终通过该类重写的equals
方法实现恶意攻击,其中equals
方法传入的pageContext
对象是为了便于获取到请求和响应对象,需要注意的是冰蝎的命令执行等参数不会从请求中获取,而是直接插入到了类成员变量中。
冰蝎后门示例
BCEL ClassLoader
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,这个项目下出现过比较出名的反序列化漏洞,我们常常说的CC链就是出自他的另一个子项目Apache Commons Collections。BCEL库提供了一系列用于分析、创建、修改Java Class文件的API,它被包含在了原生的JDK中,BCEL的类加载器在解析类名的时候会对ClassName中有$$BCEL$$
标识的类做特殊处理,该特性经常被用于编写各种攻击payload。
BCEL攻击原理
Oracle JDK引用了BCEL库,不过修改了原包名org.apache.bcel.util.ClassLoader
为com.sun.org.apache.bcel.internal.util.ClassLoader
,当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这个特性仅适用于BCEL 6.0以下,因为从6.0开始org.apache.bcel.classfile.ConstantUtf8#setBytes
就已经过时了
BCEL编解码
我们可以通过BCEL提供的两个类Repository
和Utility
来加载字节码: Repository
用于将一个Java Class转换成原生字节码(javac命令也可以);Utility用于将原生的字节码转换成BCEL格式的字节码。
BCEL编码:
这里我们就是用之前已经编码过的一个类的字节码进行测试
BCEL解码:
将刚才编码的代码进行解码
如果被加载的类名中包含了$$BCEL$$
关键字,BCEL就会使用特殊的方式进行解码并加载解码之后的类。
加载字节码
我们完成编码之后可以进行加载类的操作
ClassLoader#defineClass加载字节码
无论我们是网络加载class文件还是本地加载class文件,java会经历三个方法的调用
ClassLoader#loadClass-->ClassLoader#findClass-->ClassLoader#defineClass
- loadClass的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行findClass
- findClass的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给defineClass
- defineClass的作用是处理前面传入的字节码,将其处理成真正的Java类
所以其实真正加载类的方法是defineClass
接下来我们就编写代码测试加载字节码
TestHello.java
使用javac对该文件进行编译,然后使用cat TestHello.class
获取其base64值
编写代码加载类
利用TemplatesImpl加载字节码
利用defineClass来加载字节码我们刚刚已经介绍过了,虽然defineClass方法可以加载字节码,但是大部分开发者不会选择直接使用,但是我们可以找到另外的出路,那就是TemplatesImpl
用到了defineClass方法。在多个反序列化链中,以及fastjosn等组件中,都会有TemplatesImpl
身影。
TemplatesImpl
中有一个_bytecodes
成员变量,用于存储类字节码,通过JSON反序列化的方式可以修改该变量值,但因为该成员变量没有可映射的get/set方法所以需要修改JSON库的虚拟化配置,比如Fastjson解析时必须启用Feature.SupportNonPublicField
、Jackson必须开启JacksonPolymorphicDeserialization
(调用mapper.enableDefaultTyping()
),所以利用条件相对较高。
在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类中定义了内部类TransletClassLoader
我们可以看到这里类里自定义了一个defineClass方法,不过其内部还是调用的ClassLoader的defineClass方法,我们可以看到这个定义的内部类没有声明作用域,所以采用的是default
作用域
所以该方法我们可以在同一个包中进行调用。
这里我们从TransletClassLoader#defineClass()
开始,向上寻找调用链,最终完成的调用链如下
最外面的两层TemplatesImpl#newTransformer()
以及TemplatesImpl#getOutputProperties()
都是public修饰的,因此可以被外部调用
我们使用TemplatesImpl#newTransformer
编写一个简单的poc
newTransformerTest.java
setFieldValue方法用来设置私有属性,这里是直接调用ysoserial.payloads.util.Reflections.setFieldValue
。这里设置了三个属性:_bytecodes
、_name
、_tfactory
。_bytecodes
是由字节码组成的数组,用来存放恶意字节码;_name
可以是任意字符串,只要不为空就好,_tfactory
需要是一个TransformerFactoryImpl
对象,因为TemplatesImpl
的defineTransletClasses()
方法调用了_tfactory.getExternalExtensionsMap()
,如果是null则会报错。另外需要注意的是,TemplatesImpl
中加载的字节码必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类。
我们在构造中执行了super.transletVersion = 101;
这个代码是因为,在我们这里该值为100,我们在运行newTransformerTest.java
代码时,走到obj.newTransformer();
这里会将该值和VER_SPLIT_NAMES_ARRAY
(值101)比较,导致进入判断,但是namesArray的值为null,最终报错,如果我们将值修改为101就不会出现如下问题。
正常执行结果:
Unsafe
Unsafe
提供了非常底层的内存、CAS、线程调度、类、对象
等操作、Unsafe
正如它的名字一样它提供的几乎所有的方法都是不安全的。
Unsafe创建对象
我们通过查看源码可以发现其存在getUnsafe()方法,但是使用该方法获取Unsafe实例还会检查类加载器,默认情况下只允许Bootstrap Classloader
调用。而且其构造方法是私有的,所以也无法通过new
方式创建Unsafe实例。
我们继续阅读代码可以发现其静态代码块中存在实例化对象。
尽管正常创建方式是无法创建,但是我们可以通过反射的方式创建对象。
使用反射获取构造器创建对象
使用反射获取theUnsafe成员变量
使用allocateInstance创建实例
Unsafe.allocateInstance(Class<?> cls)
方法提供绕过任意构造器实例化的功能,一些反射也无法创建创建实例的类,我们就可以使用Unsafe
的allocateInstance
方法就可以绕过这个限制了。
测试类代码
使用Unsafe创建对象
输出结果:
我们通过上图中的输出就可以看到,其是执行了Hello方法中的代码,构造器中的代码并没有被执行。
接着我们将其构造方法私有化尝试获取对象
使用Unsafe创建对象
通过输出结果可以看到,成功创建对象,也是没有触发构造器。
使用defineClass加载字节码创建类
Unsafe
提供了一个通过传入类名、类字节码的方式就可以定义类的defineClass
方法
使用Unsafe创建测试对象
或调用需要传入类加载器和保护域的方法:
传入六个参数:类名、字节码、字节码起始、字节码长度、加载器、保护域
Unsafe
还可以通过defineAnonymousClass
方法创建内部类
测试实例
这里使用的字节码还是之前我们使用的TestHello,创建实例会调用计算器
输出结果
参考文章
https://www.cnblogs.com/chengez/p/ClassLoader.html
https://www.cnblogs.com/shiblog/p/15896520.html
https://javasec.org/javase/ClassLoader/
https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
https://blog.csdn.net/nobaldnolove/article/details/125819901