类加载机制(ClassLoader)
ClassLoader简介
Java依赖于JVM(Java Virtual Machine)来实现跨平台开发语言。Java程序编译为class字节码文件,当Java类初始化时会通过java.lang.ClassLoader
加载class字节码文件,ClassLoader
调用JVM的native方法声明一个类的实例。
ClassLoader是所有类加载器的父类
JVM顶层ClassLoader:
Bootstrap ClassLoader
(引导加载器)Extension ClassLoader
(拓展类加载器)App ClassLoader
(系统类加载器)
默认使用App ClassLoader
加载类
- Tips:当使用
getClassLoader
返回的ClassLoader为Bootstrap ClassLoader
时,显示为null
(Bootstrap ClassLoader实现于JVM层,使用c++编写)
ClassLoader的方法:
loadClass
(加载指定的Java类)findClass
(查找指定的Java类)findLoadedClass
(查找JVM已经加载过的类)defineClass
(定义一个Java类)resolveClass
(链接指定的Java类)
类的加载方式
类的加载分为显式和隐式
显式为
类名.方法名()
、new 类名()
隐式
1
2
3Class.forName("java.lang.Runtime");
foo1.getClass().getClassLoader().loadClass("java.lang.Runtime")Class.forName("类名")
默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器)
,而ClassLoader.loadClass
默认不会初始化类方法。
ClassLoader类加载流程
ClassLoader
调用loadClass()
加载类,先调用findLoadedClass()
查询是否加载了该类,如果已加载则返回类对象,没有则进行下一步- 如果传入了父类加载器,比如通过
this.getClass().getClassLoader().loadClass(...)
这种方式进行类的加载的话,则使用getClassLoader()
返回的父类加载器进行加载,否则使用Bootstrap ClassLoader
ClassCloader
调用findClass()
试图找到该类的字节码(如果没有重写findClass()
方法则返回异常),并使用defineClass()
方法进入JVM注册类并返回类对象(Class)
- Tips:如果
loadClass()
时使用了resolve参数,还需要调用resolveClass
方法链接类
自定义ClassLoader加载类(defineClass)
理论上只要重写了继承了ClassLoader
类的类的findClass
方法就可以实现加载目录class文件,可用于加载一些不在ClassPath
的类或者后面的URLClassLoader
类加载远程资源
TestHelloWord.java文件
1 | package com.test.sample5; |
TestClassLoader.java文件
1 | package com.test.sample5; |
字节码可用以下代码生成
1 | Files.readAllBytes(Paths.get("evil.class")) |
或者javassist
1 | ClassPool pool = ClassPool.getDefault(); |
注意要将TestHelloWorld.java
文件移到ClassPath
之外的路径,这样才会使用TestClassLoader
文件里面的findclass()
方法进行类加载。
也可以通过反射的方式直接调用ClassLoader#defineClass方法,需要将defineClass设置可访问权限
字节码的base64可以使用命令生成cat HelloClass.class|base64|tr -d “\n”
1 | package com.govuln; |
注意:编译java文件的java版本和加载字节码的java文件的java版本要一样
defineClass不会触发构造方法,需要使用
newInstance
或者其他能够调用构造函数的方式触发用途:在webshell中实现恶意对象的构造,或者本地命令执行漏洞调用自定义的native方法绕过RASP(Runtime Application Self-Protection)检测
利用TemplatesImpl加载类
TemplatesImpl类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,在fastjson的payload中很常见利用的一个类,由于存在一条gadget可以调用defineClass方法,因此可以加载任意类,造成代码执行等。
调用链
1 | TemplatesImpl#getOutputProperties->TemplatesImpl#newTransformer->TemplatesImpl#getTransletInstance->TemplatesImpl#defineTransletClasses->TemplatesImpl.TransletClassLoader#defineClass |
在TemplatesImpl的内部类TransletClassLoader中存在一个没有修饰符(默认为default,可以被同一个包的方法调用)的defineClass方法,传入字节数组byte[]作为变量,加载字节码。
向上回溯,在defineTransletClasses方法中调用了defineClass,在此之前,_tfactory
需要指定为TransformerFactoryImpl对象,_bytecodes
就是需要加载的字节码
较为关键的是getTransletInstance方法,先通过defineTransletClasses加载字节码,然后使用newInstance新建对象,到这里已经符合所有条件,可以通过自定义类,转换成字节码进行加载
继续向上查找方法的调用点,追溯到getOutputProperties或者newTransformer进行调用
另外需要注意的是,加载的类必须是AbstractTranslet的子类,总结起来条件如下
_name
不为null_bytecodes
为字节码序列_tfactory
为TemplateImpl对象- 类继承了AbstractTranslet类
构造需要加载的TemplatePOC类
1 | import com.sun.org.apache.xalan.internal.xsltc.DOM; |
显示调用TemplatesImpl类的newTransformer或getOutputProperties方法,这里使用yaoserial的setFieldValue
方法设置私有变量的值
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
运行poc成功加载类,执行了构造函数中的内容
利用BCEL加载类
更详细的在p牛的博客BCEL ClassLoader去哪了
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目。BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。其中ClassLoader包含在在Java 8u251及以前的的原生JDK中
Repository
用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码;Utility
用于将 原生的字节码转换成BCEL格式的字节码BCEL ClassLoader
用于加载这串特殊的“字节码”,并可以执行其中的代码
创建需要加载的evil类
1 | public class evil { |
进行编码和解码
1 | import com.sun.org.apache.bcel.internal.Repository; |
成功执行代码
Fastjson BCEL加载类
以下内容摘自p牛的博客:
当年Fastjson反序列化漏洞出现后,网络上广泛流传的利用链有下面三个:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
com.sun.rowset.JdbcRowSetImpl
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
第一个利用链是常规的Java字节码的执行,但是需要开启Feature.SupportNonPublicField
,比较鸡肋;第二个利用链用到的是JNDI注入,利用条件相对较低,但是需要连接远程恶意服务器,在目标没外网的情况下无法直接利用;第三个利用链也是一个字节码的利用,但其无需目标额外开启选项,也不用连接外部服务器,利用条件更低。
BasicDataSource
的利用原理可以参考这篇文章https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html,本文也仅做一个简单的描述。
BasicDataSource
的toString()
方法会遍历这个类的所有getter并执行,于是通过getConnection()->createDataSource()->createConnectionFactory()
的调用关系,调用到了createConnectionFactory
方法:
1 | protected ConnectionFactory createConnectionFactory() throws SQLException { |
在createConnectionFactory
方法中,调用了Class.forName(driverClassName, true, driverClassLoader)
。有读过我的《Java安全漫谈》第一篇文章的同学应该对Class.forName
还有印象,第二个参数initial
为true时,类加载后将会直接执行static{}
块中的代码。
因为driverClassLoader
和driverClassName
都可以通过fastjson控制,所以只要找到一个可以利用的恶意类即可。
BCEL ClassLoader闪亮登场。第二章中我们已经演示过com.sun.org.apache.bcel.internal.util.ClassLoader
是如何加载字节码并执行命令的,这里只是将前文的loadClass
变成了Class.forName
。
综上,我们可以构造一个Fastjson的POC:
1 | { |
触发反序列化命令执行:
URLClassLoader
通过URLClassLoader
加载远程jar实现远程的类方法调用
正常情况下,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协议
加载Jar包
CMD.jar文件包含class文件对应编译前的代码如下
1 | import java.io.IOException; |
将class打包成jar文件的教程:https://blog.csdn.net/weixin_43315211/article/details/89708286
TestURLClassLoader.java文件
1 | package com.test.sample5; |
也可以不打包为jar包,直接使用urlclassloader加载class文件
加载Class文件
- 将Hello.class文件放置在vps上
1 | package com.govuln; |
反射
Class类的获取
在java中,万事万物皆对象,类也是对象,类为Class的对象,官方叫法为Class type
,翻译过来是类类型。
结合例子理解
1 | package com.test.reflect; |
输出结果如下
获取Class类类型需要通过隐式的方式获取,因为Class的构造方法为private
隐式为如下几种
1 | #通过类对象的class属性获取 |
Java动态加载类
- java程序编译为class文件时加载类,称为
静态加载
,此时java会加载程序中出现的所有类。 - 在程序运行时临时加载一个类比如通过用户输入,args接收参数需要加载的类,称为
动态加载
实现某些功能的类一般使用动态加载
反射获得信息
流程:获得类的Class type=>获得类的方法、变量、构造函数
1 | #获得类的class type |
getMethods
获得包括类的父类继承的方法getDeclaredMethods
仅获得类自己声明的方法getFields
获得包括类的父类继承的变量getDeclaredFields
获得类自己的变量
反射执行方法
流程:获得类的Class type=>获得类的构造函数=>通过构造函数获得实例、获得方法=>通过方法、实例反射调用方法
方法正常调用:
1 | System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(), "UTF-8")); |
方法通过反射调用:
1 | import java.io.ByteArrayOutputStream; |
如果不需要创建对象,可以简化为以下写法
1 | import java.lang.reflect.Method; |
setAccessible(true)
是为了获得Runtime构造方法的访问权限,因为Runtime的构造方法是private修饰符getMethod()
第一个参数为方法名,第二个参数为方法参数的Class type,例如这里是String.class
,如果有多个变量可以采用new Object[]{String.class,String.class}
获得getMethod("exec",String.class,String.class)
来传入invoke()
的第一个变量为实例(如果调用的是静态方法也可以为null),后面的变量为方法的参数,如果有多个参数可以new Object[]{"1","2"}
的方式传入
ProcessBuilder的反射
1 | import jdk.internal.org.objectweb.asm.commons.Method; |
反射总结
反射可以获得类的实例对象、方法、构造函数、变量
Java的大部分框架都是采用了反射机制来实现的(如:
Spring MVC
、ORM框架
等),Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。