Java基础:再谈ClassLoader
最后更新时间:
文章总字数:
预计阅读时间:
Java基础:再谈ClassLoader
前言
本系列是看https://javasec.org
网站文章的心得和笔记,部分摘抄原文,不知道能不能坚持把文章看完呢…不过如果看完了,代码也跟着敲一遍,一定可以成为javasec高手(笑)。
Java是一个依赖于JVM
(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件
,Java类初始化的时候会调用java.lang.ClassLoader
加载类字节码,ClassLoader
会调用JVM的native方法(defineClass0/1/2
)来定义一个java.lang.Class
实例。
Java类
Java是编译型语言,我们编写的.java后缀的文件并不能直接运行,需要先编译成.class后缀的文件才能被JVM运行,JVM从.class文件中读出二进制字节码,再使用classLoader加载。
先使用javac命令编译文件,将.java转化为.class,为了了解到java类文件的具体内容,我们可以使用javap来反汇编java类文件,即将原本类文件中的字节码显示为可读文本:
ClassLoader
一切的Java类都必须经过JVM加载后才能运行,而ClassLoader
的主要作用就是Java类文件的加载。
在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)
、Extension ClassLoader(扩展类加载器)
、App ClassLoader(系统类加载器)
,AppClassLoader
是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类。
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类)findClass
(查找指定的Java类)findLoadedClass
(查找JVM已经加载过的类)defineClass
(定义一个Java类)resolveClass
(链接指定的Java类)
ClassLoader类加载流程
ClassLoader
加载TestHelloWorld
类loadClass
重要流程如下:
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
java.lang.ClassLoader
是所有的类加载器的父类,java.lang.ClassLoader
有非常多的子类加载器,我们也可以自己写一个类加载器来实现加载自定义的字节码(这里以加载TestHelloWorld
类为例),并调用hello方法。其实自写也只是重载了几个函数,并不是全部功能重新实现一遍。
当这个TestHelloWorld
存在于本地时,我们可以直接创建对象并调用hello方法,但是现在我们只有字节码,于是可以通过自定义类加载器来重写findClass
方法,然后在调用defineClass
方法的时候传入TestHelloWorld
类的字节码的方式来向JVM中定义一个TestHelloWorld
类,最后通过反射机制即可调用hello方法。
1 | package com.test.classloader; |
利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象。这里使用的是通过字节码来注册类,下面演示使用URLClassLoader获取远程类。
URLClassLoader
URLClassLoader
继承了ClassLoader
,URLClassLoader
提供了加载远程资源的能力,在写漏洞利用的payload
或者webshell
的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。
首先我们可以先打包一个jar包,流程是先写一个用于命令执行的CMD.class,里面有一个exec方法,等一下远程加载之后可以通过反射调用:
1 | import java.io.IOException; |
然后javac命令编译成.class文件,jar cvf CMD.jar CMD.class
打包成jar包,最后python起一个本地http服务。
打包完之后可以写通过URLClassLoader来远程加载这个jar包。
1 | package com.test.classloader; |
JSP自定义类加载后门
以冰蝎为首的JSP后门(说是jsp后门是因为这是个jsp文件,其实里面都是加个标签然后写java代码)利用的就是自定义类加载实现的,冰蝎的客户端会将待执行的命令或代码片段通过动态编译成类字节码并加密后传到冰蝎的JSP后门,后门会经过AES解密得到一个随机类名的类字节码,然后调用自定义的类加载器加载,最终通过该类重写的equals
方法实现恶意攻击。
使用字节码并且加密,基本不可能被杀软查到,如果使用明文exec的话肯定被杀掉,学习学习冰蝎的JSP后门是怎么写的:
1 | <%import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %> |
BCEL ClassLoader
BCEL简介与攻击展示
BCEL是一个用于分析、创建和操纵Java类文件的工具库,BCEL的类加载器在解析类名时会对ClassName中有$$BCEL$$
标识的类做特殊处理,该特性经常被用于编写各类攻击Payload。
BCEL的类加载器在rt.jar/com/sun/org/apache/bcel/internal/util/
包下(JDK<8u251),可以实现加载字节码并初始化一个类的功能,该类也继承了原生的Classloader类,但是重写了loadClass()
方法,看看关键部分代码:
可以看到首先会判断类名是否以$$BCEL$$
开头,之后调用createClass()
方法拿到一个JavaClass
对象最终通过defineClass()
加载字节码还原类。
可以写一个demo打断点来调试,看看具体发生了什么。
先写一个恶意类用来执行命令
1 | package com.test.classloader; |
写BCELDemo,同时也生成了code的payload(其实就只是把字节码进行了utility.encode),加上$$BCEL$$
即可打远程
1 | package com.test.classloader; |
本地测试成功,打上断点开始调试
BCEL攻击流程
直接进入到classLoader的关键代码处,可以看到这里判断是否为$$BCEL$$
开头,然后调用createClass
跟进createClass:
这里就可以看出来为什么我们要使用Utility.encode来加密字节码,因为createClass里面通过decode来获得bytes数组,跟进代码,可以发现在createClass里面就已经获得了我们恶意类。
从createClass出来后通过获得的恶意类再重新获得字节码,注册到JVM中,完成loadClass。从classLoader出去之后就进行newInstance,静态恶意代码得以执行。
总结:流程非常简单,加密字节码,BCEL再解密获取类,下面看看如何在实战中应用BCEL ClassLoader对Java web应用进行攻击。
BCEL FastJson攻击链分析
之前复现了FastJson的攻击链,有一种方法是JNDI注入,但是JNDI是远程类加载,如果机器无出网就没办法使用这个方法,只能动态加载恶意代码。
另一条链TemplatesImpl利用链虽然原理上也是利用了 ClassLoader 动态加载恶意代码,但是需要开启Feature.SupportNonPublicField(因为需要通过json修改private变量),并且实际应用中其实不多见。所以我们可以采用另外一种攻击方法apache-BCEL,直接传入字节码不需要出网就可执行恶意代码但是需要引入tomcat的依赖,tomcat在实际攻击中还算是比较常见的。
搭个环境分析一下,下面是poc代码:
1 | <dependency> |
1 | public static void main(String[] args) throws Exception { |
BCEL攻击流程在上文已经分析过了,这里不做赘述,这里主要讨论的问题是如何将BCEL和FastJson结合起来,关键是通过FastJson解析自动调用BCEL的classLoader,我们需要结合tomcat.dbcp.dpcp2里的BasicDataSource类,关键方法是getConnection,反序列化的时候会自动调用。
返回时调用了createDataSource:
跟进createConnectionFactory方法:
可以看到这里的关键调用forName方法,具体逻辑是使用这个ClassLoader并传入这个driverClassName,那我们只需要通过json字符串来赋值成我们需要的BCEL的ClassLoader,driverClassName传入加密了的字节码即可完成攻击。payload如下:
1 | { |
Xalan ClassLoader
Xalan和BCEL一样都经常被用于编写反序列化Payload,Xalan最大的特点是可以传入类字节码并初始化(需要调用getOutputProperties
方法),从而实现RCE,比如Fastjson和Jackson会使用反射调用getter/setter
或成员变量映射
的方式实现JSON反序列化。
事实上,在上一篇文章中已经非常完整地分析过了这个ClassLoader怎么利用,其实就是利用TemplatesImpl类来动态加载字节码。
但是我们在上面也说了,这种方法其实局限性很大,远不如BCEL泛用。TemplatesImpl
中有一个_bytecodes
成员变量,用于存储类字节码,通过JSON反序列化的方式可以修改该变量值,但因为该成员变量没有可映射的get/set方法,所以需要修改JSON库的虚拟化配置,比如Fastjson解析时必须启用Feature.SupportNonPublicField
、Jackson必须开启JacksonPolymorphicDeserialization
(调用mapper.enableDefaultTyping()
),所以利用条件相对较高。
Xalan FastJson攻击链分析
详见上一篇文章,FastJson全系漏洞研究中的TemplatesImpl攻击链分析。
ClassLoader总结
ClassLoader
是JVM中一个非常重要的组成部分,也是我们研究java安全很重要的一个方面,ClassLoader
可以为我们加载任意的java类,通过自定义ClassLoader
更能够实现自定义类加载行为,在后面的几个章节我们也将继续分析ClassLoader
的实际利用场景
本次再探ClassLoader
,分析了JVM加载原理与ClassLoader加载流程,自定义了ClassLoader,研究了冰蝎jsp后门以及两个危险的自写的ClassLoader分析,以及实际环境中的利用链,可以说这次研究的是比较全面了。