Java安全基础第二篇之ClassLoader
背景
ClassLoader
顾名思义为类的加载,类的加载指的是将类的.class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class
对象,用来封装类在方法区内的数据结构。类的加载的最终形态是位于堆区中的Class
对象,Class
对象封装了类在方法区内的数据结构,并且向程序员提供了访问方法区内的数据结构的接口。加载之后这个类的信息就会被放到内存中,但是是否对这个类进行初始化就要看这个类实际的定义与使用场景了,后面会一一介绍,先看看ClassLoader
的介绍与使用。
ClassLoader概念定义
中文文档中对ClassLoader
的定义如下:
可以总结出这个类的作用就是根据一个指定的类名,找到对应的Class
字节码文件,然后加载成一个java.lang.Class
类的一个实例。
ClassLoader分类
大部分java
程序会使用以下3中系统提供的类加载器:
启动类加载器(Bootstrap ClassLoader):
这个类加载器负责将\lib
目录下的类库加载到虚拟机内存中,用来加载java
的核心库,此类加载器并不继承于java.lang.ClassLoader
,不能被java
程序直接调用,代码是使用C++
编写的,是虚拟机自身的一部分。
扩展类加载器(Extendsion ClassLoader):
这个类加载器负责加载\lib\ext
目录下的类库,用来加载java
的扩展库,开发者可以直接使用这个类加载器。
应用程序类加载器(Application ClassLoader):
这个类加载器负责加载用户类路径CLASSPATH
下的类库,一般我们编写的java
类都是由这个类加载器加载,这个类加载器是ClassLoader
中的getSystemClassLoader()
方法的返回值,所以也称为系统类加载器。一般情况下这就是系统默认的类加载器。(java.io.File.class.getClassLoader()
将返回一个null
对象,因为java.io.File
类在JVM
初始化的时候会被Bootstrap ClassLoader
加载,我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader
时候都会返回null
)
除此之外,我们还可以加入自己定义的类加载器,以满足特殊的需求,需要继承java.lang.ClassLoader
类。在实际的攻击场景中,比如本地命令执行漏洞调用自定义类字节码的native
方法可绕过RASP
检测。
类加载器之间的层次关系如下图:
Java类加载分类
上面ClassLoader
只是java
类加载的一种方法,Java
类加载方式可以分为显式和隐式,显式加载即我们通常使用Java
反射或者ClassLoader
来动态加载一个类对象,而隐式加载指的是类名.方法名()
或new
类实例。显式加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。
对于动态加载的2
种情况,会存在一些细微的差别:
Class.forName()和ClassLoader.loadClass()区别
Class.forName()
:除了将类的.class
文件加载到jvm
中之外,还会对类进行解释,执行类中的static
块Class.forName(name, initialize, loader)
带参函数也可控制是否加载static
块
ClassLoader.loadClass()
:只干一件事情,就是将.class
文件加载到jvm
中,不会执行static
中的内容,只有在newInstance
才会去执行static
块,实际测试代码如下:
1 | /** |
Java类加载机制
在之前反射那里学过,类可以通过多种方式进行加载,以一个Java
的HelloWorld
来学习ClassLoader
:
1 | public class TestHelloWorld { |
ClassLoader
加载TestHelloWorld
类重要流程如下:
ClassLoader
会调用public Class<?> loadClass(String name)
方法加载TestHelloWorld
类。- 调用
findLoadedClass
方法检查TestHelloWorld
类是否已经初始化,如果JVM
已初始化过该类则直接返回类对象。 - 如果创建当前
ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
)就使用父类加载器加载TestHelloWorld
类,否则使用JVM
的Bootstrap ClassLoader
加载。 - 如果上一步无法加载
TestHelloWorld
类,那么调用自身的findClass
方法尝试加载TestHelloWorld
类。 - 如果当前的
ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的TestHelloWorld
类名找到了对应的类字节码,那么应该调用defineClass
方法去JVM
中注册该类。 - 如果调用
loadClass
的时候传入的resolve
参数为true
,那么还需要调用resolveClass
方法链接类,默认为false
。 - 返回一个被
JVM
加载后的java.lang.Class
类对象。
自定义ClassLoader
假设我们需要的类不存在于classpath
,那就可以用自定义类加载器重写findClass
方法,然后在调用defineClass
方法的时候传入TestHelloWorld
类的字节码的方式来向JVM中定义一个TestHelloWorld
类,最后通过反射机制就可以调用TestHelloWorld
类的hello
方法了
1 | import java.lang.reflect.Method; |
URLClassLoader
使用URLClassLoader
可以更灵活的加载远程资源的能力,在写漏洞利用的payload
或者webshell
的时候我们可以使用这个特性来加载远程的jar
来实现远程的类方法调用。
1 | import java.io.ByteArrayOutputStream; |
将CMD.java
编译后放在某个目录下修改上面的path
即可,CMD
内容如下:
1 | import java.io.IOException; |
运行截图:
类的加载与初始化
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。
上面说的加载都只是如何将类加载到内存中,完成加载和连接之后是否需要初始化则需要根据当前场景进行判断,对于是否初始化,jvm
的规范严格限定了只有5种情况必须对类进行初始化:
1、遇到new
、getstatic
、putstatic
或invokestatic
这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的Java场景是:
1)使用关键字new
实例化对象的时候
2)读取或设置一个类的静态字段(被final
修饰、已在编译期把结果放入常量池的静态字段除外)的时候
3)调用一个类的静态方法的时候
2、使用java.lang.reflect
包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
4、当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main()
方法的那个类),虚拟机会先初始化这个类
5、当使用JDK1.7
的动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
以上这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。如:
1)通过子类引用父类的静态字段,不会导致子类初始化
2)通过数组定义来引用类,不会触发此类的初始化
3)常量(static final
修饰的)在编译阶段会存入调用类的常量池中,因此调用其常量本质上并没有直接引用到定义常量的类,因此不会触发常量的类的初始化
理解这个初始化有什么用呢?以fastjson
的利用为例,我们一般需要编译一个命令执行的java
文件,这里一般的写法是这样:
1 | import java.lang.Runtime; |
然后会让服务器来请求远程加载TouchFile
类,进而达到远程命令执行的效果。那么这里的写法为什么不是定义一个runtime
方法然后在main
里面调用呢?
这就牵扯到了加载初始化的过程,因为就这个攻击方法而言,我们只能让服务器来加载并初始化这个类,并不能让服务器去执行某个方法,所以这里的写法是将需要运行的代码片段放在了static
里面,然后我们可以看到在fastjson
的源码中,加载类的方法都是通过Class.forname
进行的,结合上面讲到的Class.forname
本身的加载机制也就可以理解这么写的原因了。
总结
了解一下类加载机制的灵活性,在某些限制条件下可以利用这个特性进行绕过,同时学习一下类加载之后的初始化过程,知道为什么fastjson
的payload
需要按照使用static
去修饰。