前言
Java是一个依赖于JVM实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVM的native方法来定义一个java.lang.Class实例。
类加载器分类
JVM默认类加载器
在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)、Extension ClassLoader(扩展类加载器)和App ClassLoader(系统类加载器),AppClassLoader是默认的类加载器,当不指定类加载器的情况下进行类加载时,默认会使用AppClassLoader加载类。
BootstrapClassLoader
引导类加载器BootstrapClassLoader底层原生代码由C++语言进行编写,属于JVM一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心Java库,即JVM本身,存储在/jre/lib/rt.jar目录当中,该目录当中的类都是由BootstrapClassLoader来加载。
同时,为了安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类。
ExtensionsClassLoader
扩展类加载器ExtensionsClassLoader由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载Java的扩展库。
AppClassLoader
系统类加载器AppClassLoader由sun.misc.Launcher$AppClassLoader实现,一般通过java.class.path或者Classpath环境变量来加载Java类。通常使用这个加载类来加载Java应用类,ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader。
自定义类加载器
除了上述Java自带提供的类加载器,还可以通过继承java.lang.ClassLoader类的方式实现自定义类加载器。
在ClassLoader里面有三个重要的方法: loadClass、findClass和defineClass。
loadClass方法是加载目标类的入口,它首先会查找当前ClassLoader以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载。当双亲都无法进行加载时,会调用findClass方法让自定义加载器自己来加载目标类,ClassLoader的findClass方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码,拿到字节码后再调用defineClass方法将字节码转换成Class对象。
Class.forName和ClassLoader.loadClass都是常见的类动态加载方式,但是它们之间也有着区别。
- 那就是Class.forName方法可以获取原生类型的Class,而ClassLoader.loadClass方法则会报错。
- Class.forName方法默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName(“类名”, 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法。
自定义加载器实现过程伪代码如下:
1 | class ClassLoader { |
ClassLoader类加载流程
- 首先,ClassLoader会调用loadClass方法加载类
- loadClass方法先调用findLoadedClass方法检查类是否已经初始化,如果JVM已初始化过该类则直接返回类对象
- 如果创建当前ClassLoader时传入了父类加载器,就使用父类加载器加载类,否则使用Bootstrap ClassLoader进行加载
- 如果上一步无法加载类,那么调用自身的findClass方法尝试加载类
- 如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常;如果当前类重写了findClass方法并通过传入的类名找到了对应的类字节码,那么调用defineClass方法去JVM中注册该类
- 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false
- 最后,返回一个被JVM加载后的java.lang.Class类对象
双亲委派机制
由于AppClassLoader只负责加载Classpath下的类库,因此当AppClassLoader遇到没有加载的系统类库时,会将系统类库的加载工作交给BootstrapClassLoader和ExtensionClassLoader,这就是双亲委派。
在下图中,AppClassLoader在加载一个未知的类名时,并不是立即去搜寻Classpath,它会首先将这个类名称交给ExtensionClassLoader来加载,如果ExtensionClassLoader可以加载,那么AppClassLoader就不会进行加载,否则的话AppClassLoader会搜索Classpath。ExtensionClassLoader在加载一个未知的类名时,也并不是立即搜寻ext路径,它会首先将类名称交给BootstrapClassLoader来加载,如果BootstrapClassLoader可以加载,ExtensionClassLoader也不会对其进行加载,否则的话才会搜索ext路径下的jar包。
AppClassLoader、ExtensionClassLoader、BootstrapClassLoader三者之间形成了一个级联的父子关系,优先把任务交给其父亲,当其父亲无法完成任务时才会轮到自己,在每个ClassLoader对象的内部都会存在一个parent属性指向自己的父加载器。
还需要注意的一点是,上图中的ExtensionClassLoader的parent指针是画的虚线,这是因为它的parent的值是null,当parent字段是null时,表示它的父加载器是根加载器,当Class对象的classLoader属性值是null时,就表示这个类也是根加载器加载的。
动态加载字节码
URLClassLoader加载远程字节码文件
URLClassLoader实际上是平时默认使用的AppClassLoader的父类,正常情况下Java会根据配置项sun.boot.class.path和java.class.path中列举到的基础路径(这些路径是经过处理后的java.net.URL类)来寻找class文件来加载,而这个基础路径又分为三种情况。
1 | [1] URL未以斜杠/结尾,则认为是一个JAR文件,使用JarLoader来寻找类,即为在Jar包中寻找.class文件 |
注意,要利用基础的Loader类来寻找这一点必须是非file协议的情况下,JAVA默认提供了对file,ftp,gopher,http,https,jar,mailto和netdoc协议的支持。
- 恶意类
1 | import java.io.*; |
- 利用HTTP服务加载class文件
1 | package org.example.classloader; |
ClassLoader加载字节码
不管是加载远程class文件还是本地的class或jar文件,Java都经历的是下面这三个方法的调用: ClassLoader#loadClass–>ClassLoader#findClass–>ClassLoader#defineClass。
1 | [1] loadClass的作用是从已加载的类缓存,父加载器等位置寻找(双亲委派机制),在前面没有找到的情况下,执行findClass |
编译恶意类的class文件,然后用defineClass去加载它。需要注意的是,ClassLoader#defineClass返回的类并不会初始化,只有这个对象显式地调用其构造函数初始化代码才能被执行,所以需要想办法调用返回的类的构造函数来执行命令。在实际场景中,由于defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是攻击链TemplatesImpl的基石。
1 | package org.example.classloader; |
TemplatesImpl加载字节码
上文提到了,ClassLoader#defineClass方法只能通过反射调用,在实际环境中很难有利用场景。在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类中的内部类TransletClassLoader重写了defineClass,并且该处没有显式地声明其定义域。在Java中,默认情况下,如果一个方法没有显式声明作用域,则其作用域为default,也就是说这里的defineClass由其父类的protected类型变成了一个允许被外部调用的default类型方法。
但是TransletClassLoader是内部类,只允许TemplatesImpl类中的方法调用,跟进一下发现TemplatesImpl类中只有方法TemplatesImpl#defineTransletClasses用到了TransletClassLoader类,但是TemplatesImpl#defineTransletClasses方法是private类型。
继续跟一下TemplatesImpl#defineTransletClasses方法,一共有三个方法调用了它。
1 | private synchronized Class[] getTransletClasses() {} |
接着上面的三个利用点进一步分析,getTransletIndex方法可以直接作为一个触发的点,但是测试后并没有成功触发,而getTransletClasses方法在TemplatesImpl类中已经没有继续被调用了,因此只剩下getTransletInstance方法,再跟一下发现TemplatesImpl#newTransformer方法调用了它,这里也可以作为一个触发点,继续跟下去发现TemplatesImpl#getOutputProperties方法调用了TemplatesImpl#newTransformer方法,因此这里也可以作为一个触发点。
由上可以得到两条调用链:
1 | [1] TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()->TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass() |
那么为什么getTransletIndex方法无法触发呢?上文说到,在defineClass被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行。而且,即使将初始化代码放在类的static块中,在defineClass时也无法被直接调用到。所以,如果要使用defineClass在目标机器上执行任意代码,需要想办法调用构造函数。对于getTransletIndex方法,虽然有执行到defineClass,但后面并没有对这个类对象进行实例化,也就是说并没有调用其构造函数,因此无法触发漏洞。反观TemplatesImpl#newTransformer方法,在调用完TemplatesImpl#defineTransletClasses方法后又调用了newInstance构造函数,所以能够完成后续操作,触发恶意代码。
在构造利用链的过程中,还需注意以下几点:
- TemplatesImpl中对加载的字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类
1 | import com.sun.org.apache.xalan.internal.xsltc.DOM; |
- TemplatesImpl中_name属性值必须不为null
- TemplatesImpl中_tfactory属性值必须是TransformerFactoryImpl实例
TemplatesImpl完整利用链如下:
1 | package org.example.classloader; |
BCEL ClassLoader
BCEL是一个用于分析、创建和操纵Java类文件的工具库,Oracle JDK引用了BCEL库,不过将原包名org.apache.bcel.util.ClassLoader修改为com.sun.org.apache.bcel.internal.util.ClassLoader,BCEL的类加载器在解析类名时会对ClassName中有$$BCEL$$标识的类做特殊处理,该特性经常被用于编写各类攻击Payload。
BCEL兼容性问题
Oracle自带的BCEL是修改了原始的包名,因此也有兼容性问题,已知支持该特性的JDK版本为:JDK1.5-1.7、JDK8-JDK8u241、JDK9,BCEL Classloader在JDK8u251之前是在rt.jar里面。
同时在Tomcat中也会存在相关的依赖:
- Tomcat7:org.apache.tomcat.dbcp.dbcp.BasicDataSource
- Tomcat8及其以后:org.apache.tomcat.dbcp.dbcp2.BasicDataSource
BCEL这个特性仅适用于BCEL 6.0以下,因为从6.0开始org.apache.bcel.classfile.ConstantUtf8#setBytes就已经过时了,因此利用时需要注意JDK版本是否JDK8u251之后的。
1 | /** |
BCEL攻击原理
BCEL包中有一个类com.sun.org.apache.bcel.internal.util.ClassLoader重写了Java内置的ClassLoader#loadClass方法,在重写的方法中,会判断类名是否是$$BCEL$$开头,如果是的话,接着调用createClass方法拿到一个JavaClass对象,最终通过defineClass加载字节码还原类。
跟进com.sun.org.apache.bcel.internal.util.ClassLoader#createClass方法,可以看到其获取JavaClass对象时,会进行一个decode操作。
对于encode操作,直接调用com.sun.org.apache.bcel.internal.classfile.Utility#encode方法来加密恶意类即可。
测试代码
1 | package org.example.classloader; |