Java 反射和类加载器
Reflection 反射
反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。
Class 对象
Java 反射操作的是
java.lang.Class
对象
获取 Class 对象的方法:
1
2
3
4
5
6
7
8
9
10
// 1. 对于已经加载了某个类,直接通过类的class属性获取
Class<?> c1 = java.lang.Runtime.class;
// 2. 已存在某个类的实例,调用该实例的getClass()方法获取
A a = new A();
Class<?> c2 = a.getClass(); // 使用 getSuperclass 方法可获取父类Class对象
// 3. 已知一个类的全类名,可以使用 forName(String className) 来获取
Class<?> clazz = Class.forName("java.lang.Runtime");
// 4. 通过 ClassLoader 获取
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = systemClassLoader.loadClass("java.lang.Runtime");
在获取到 Class 对象后,可以使用以下方法进行操作:
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
// 获取类的信息:
getName() // 获取全类名(包名+类名)
getClassLoader() // 获取类加载器
// 获取属性:
getDeclaredField("属性名") // 获取指定属性的值
// 获取方法:
getMethod("方法名", String.class) // 获取指定的方法,后面是传递的参数类型
getDeclaredMethod("方法名", String.class)
// 获取构造器:
getConstructor() // 获取所有本类的 public 修饰的构造器
// 创建实例对象
newInstance() // 创建Class对象对应类的实例。(无法使用其它有参构造器)
getDeclaredConstructor().newInstance() // 使用无参构造器创建实例对象
getDeclaredConstructor(int.class, String.class).newInstance(1, "Test") // 有参构造器创建对象
// 调用方法(Method)
// 如果调用的方法是 static 方法。那么 invoke 方法传入的第一个参数为 null
invoke(null, args...) // 调用class类中的静态方法
invoke(obj, args...) // 调用 obj 类的方法,args... 为传入的参数值
// 获取其它信息:
getPackage: 以Package形式返回包信息
getInterfaces: 以Class[]形式返回接口信息
getAnnotations: 以Annotation[] 形式返回注解信息
getDeclaredMethods 和 getMethods 的区别
getDeclaredMethods 获取的是类自身声明的方法,包含 public、protected 和 private 方法。 getMethods 获取的是类的所有 public 方法,包括自身的和从父类继承、接口实现的 public 方法。
反射调用
反射里几个极为重要的方法:
- 获取类
forName
- 实例化类对象
newInstance
- 获取函数
getMethod
- 执行函数
invoke
例:反射调用 java.lang.runtime 执行命令
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
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.StringJoiner;
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = systemClassLoader.loadClass("java.lang.Runtime");
// Class<?> clazz = Class.forName("java.lang.Runtime");
Constructor<?> runtime = clazz.getDeclaredConstructor();
runtime.setAccessible(true); // 关闭 Java语言的访问检查。用于获取非 public 修饰的构造方法、方法、属性
Method exec = clazz.getMethod("exec", String.class);
Process process = (Process) exec.invoke(runtime.newInstance(), "whoami");
// 读取执行结果
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"));
StringJoiner sj = new StringJoiner("\n"); // 存放命令执行结果
String line;
while ((line = bufferedReader.readLine()) != null) {
sj.add(line);
}
bufferedReader.close();
System.out.println(sj); // 输出结果
}
}
注意事项
从 Java 9 开始,就不推荐使用 newInstance()
获取实例化对象了,而是使用 getDeclaredConstructor().newInstance()
代替(获取构造器后,再创建实例)。 在获取非 public 修饰的构造器、属性或方法时,需要设置 setAccessible(true)
关闭 Java 语言的访问检查后才能获取。
ClassLoader 类加载器
注意:在加载字节码文件的时候,需要特别注意编译该字节码文件的 JDK 版本。如果加载的字节码是由不同版本的 JDK 编译的,很有可能会加载出错。
Java 是一个依赖于 JVM 实现的跨平台的开发语言。Java 程序在运行前需要先编译成 class 文件,Java 类初始化的时候会调用 java.lang.ClassLoader
加载类字节码,ClassLoader 会调用 JVM 的 native 方法(defineClass0/1/2
)来定义一个 java.lang.Class
实例。
JVM 规范定义了如下类型的类的加载器:
- 系统类加载器 (App ClassLoader) 默认的类加载器
- 扩展类加载器 (Extension ClassLoader)
- 引导类加载器 (Bootstrap ClassLoader) 该类加载器实现于 JVM 层,采用 C/C++ 编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取系统类的加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// 获取系统类加载器的父类加载器-->扩展类加载器
ClassLoader parent = systemClassLoader.getParent();
// 获取扩展类加载器的父类加载器-->引导类加载器(C/C++);引导类加载器无法直接获取
ClassLoader parent1 = parent.getParent();
System.out.println(parent1); // 输出 null
// 测试用户写的类是哪个加载器加载的
ClassLoader classLoader = Class.forName("org.example.Test").getClassLoader();
System.out.println(classLoader);
// 测试jdk内部类是哪个加载器加载的。(由引导类加载器加载的,为 null)
ClassLoader classLoader1 = Class.forName("java.lang.Object").getClassLoader();
System.out.println(classLoader1);
ClassLoader 的核心方法有:
- loadClass (加载指定的 Java 类)
- findClass (查找指定的 Java 类)
- findLoadedClass (查找 JVM 已经加载过的类)
- defineClass (定义一个 Java 类)
- resolveClass (链接指定的 Java 类)
Class.forName() 和 ClassLoader 的区别
Class.forName() 和 ClassLoader 都可用来对类进行加载。
Class.forName() 除了将类的 .class
文件加载到 JVM 中之外,还会对类进行初始化,执行类中的 static 代码块。
而 ClassLoader 只干一件事情,就是将字节码文件加载到 JVM 中,不会执行 static 中的内容,只有在 newInstance 才会去执行 static 代码块。
使用 Class.forName(name,initialize,loader)
带参数也可控制是否加载 static 代码块。并且只有调用了 newInstance() 方法采用调用构造函数,创建类的对象。
自定义 ClassLoader
自定义 ClassLoader 是为了在运行时直接加载字节码文件。
因为加载字节码文件需要使用 ClassLoader 的 defineClass()
方法,但 defineClass()
是个 final 修饰的 protected 方法,无法直接调用。
实现自定义 ClassLoader 调用 defineClass() 方法的两种方式
第一种:编写一个类,继承 ClassLoader 抽象类,复写调用父类的 defineClass() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) throws Throwable {
String filepath = "C:\\Test.class";
byte[] evilBytes = Files.readAllBytes(Paths.get(filepath));
MyClassLoader classLoader = new MyClassLoader();
Class<?> clazz = classLoader.defineClass("org.example.Test", evilBytes);
}
static class MyClassLoader extends ClassLoader {
public Class<?> defineClass(String name, byte[] b) {
// 调用父类 protected 修饰的方法,该方法支持载入外部字节码文件
return super.defineClass(name, b, 0, b.length);
}
}
}
第二种:通过反射直接调用系统 ClassLoader 的 defineClass() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
// 读取字节码文件
FileInputStream stream = new FileInputStream("C:\\Main.class");
byte[] evilBytes = new byte[stream.available()];
stream.read(bytes);
// 自定义 ClassLoader
// 从 ClassLoader.class 获取 defineClass() 方法,为自定义 ClassLoader 做准备
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
// 使用反射调用 ClassLoader 的 defineClass() 方法;将 {"org.example.Test", evilBytes, 0, evilBytes.length} 传入至 defineClass() 方法,实现了自定义 ClassLoader
// protected final Class<?> defineClass(byte[] b, int off, int len)
Class<?> clazz = (Class<?>) defineClass.invoke(ClassLoader.getSystemClassLoader(), "org.example.Test", evilBytes, 0, evilBytes.length);
URLClassLoader
java.net.URLClassLoader
其本身通过继承 java.lang.ClassLoader
类,重写了 findClass
方法从而实现了加载目录、class 文件甚至是远程资源文件。
1
2
3
4
5
6
7
8
9
10
11
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
URL url = new URL("file:/C:\\Test.class"); // http://127.0.0.1/cmd.jar
// 创建 URLClassLoader 对象,并加载远程 jar包
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class<?> clazz = urlClassLoader.loadClass("com.example.Test");
}
}
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 ClassLoader 环境限制
BCEL ClassLoader 在 JDK < 8u251 之前是在 rt.jar 里面(com.sun.org.apache.bcel.internal.util.ClassLoader),之后的 JDK 中没有了。
在 Tomcat 中也会存在相关的依赖 tomcat7 org.apache.tomcat.dbcp.dbcp.BasicDataSource
tomcat8 及其以后 org.apache.tomcat.dbcp.dbcp2.BasicDataSource
。
BCEL 攻击原理
BCEL 重写了 Java 内置的 ClassLoader#loadClass()
方法。当 BCEL 的 com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass
加载一个类名开头为 $$BCEL$$
的类时会截取出 $$BCEL$$
后面的字符串,然后使用 com.sun.org.apache.bcel.internal.classfile.Utility#decode
将字符串解析成类字节码(带有攻击代码的恶意类),最后会调用 defineClass()
注册解码后的类,一旦该类被加载就会触发类中的恶意代码。
BCEL 编码与解码
1
2
3
4
5
6
7
// 将普通字节码文件编码成BCEL编码的字节码
String className = "$$BCEL$$" + com.sun.org.apache.bcel.internal.classfile.Utility.encode(CLASS_BYTES, true);
// 从BCEL格式解码成普通字节码文件
int index = className.indexOf("$$BCEL$$");
String realName = className.substring(index + 8);
byte[] bytes = com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName, true);
native defineClass0
使用 native 修饰的方法 defineClass0 来加载字节码(使用 native 方法理论上可以绕过一些检测)。
java.lang.ClassLoader
和 java.lang.reflect.Proxy
中均有 native 修饰的 defineClass0 方法(不同 JDK 版本,defineClass0 方法有所修改;而在 java.lang.reflect.Proxy 中 defineClass0 方法更是被删除了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// java8:java.lang.ClassLoader 中的 defineClass0 方法
private native Class<?> defineClass0(String name, byte[] b, int off, int len, ProtectionDomain pd);
private native Class<?> defineClass1(String name, byte[] b, int off, int len, ProtectionDomain pd, String source);
private native Class<?> defineClass2(String name, java.nio.ByteBuffer b, int off, int len, ProtectionDomain pd, String source);
// java8:java.lang.reflect.Proxy 中的 defineClass0 方法
private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);
// java19:java.lang.ClassLoader
static native Class<?> defineClass0(ClassLoader loader, Class<?> lookup, String name, byte[] b, int off, int len, ProtectionDomain pd, boolean initialize, int flags, Object classData);
static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len, ProtectionDomain pd, String source);
static native Class<?> defineClass2(ClassLoader loader, String name, java.nio.ByteBuffer b, int off, int len, ProtectionDomain pd, String source);
JDK 8 使用这两个类调用 defineClass0 方法,例:
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
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) throws Exception {
String filepath = "C:\\Test.class";
byte[] evilBytes = Files.readAllBytes(Paths.get(filepath));
Class<?> clazz = defineClass0ByProxy("org.example.Test", evilBytes);
}
public static Class<?> defineClass0ByClassLoader(String className, byte[] classBytes) throws Exception {
java.lang.ClassLoader systemClassLoader = java.lang.ClassLoader.getSystemClassLoader();
Method defineClass0 = java.lang.ClassLoader.class.getDeclaredMethod("defineClass0", String.class, byte[].class, int.class, int.class, java.security.ProtectionDomain.class);
defineClass0.setAccessible(true);
// JDK8: private native Class<?> defineClass0(String name, byte[] b, int off, int len, ProtectionDomain pd);
Class<?> clazz = (Class<?>) defineClass0.invoke(systemClassLoader, className, classBytes, 0, classBytes.length, null);
return clazz;
}
public static Class<?> defineClass0ByProxy(String className, byte[] classBytes) throws Exception {
java.lang.ClassLoader systemClassLoader = java.lang.ClassLoader.getSystemClassLoader();
// 反射 java.lang.reflect.Proxy类获取其中的 defineClass0方法
Method defineClass0 = java.lang.reflect.Proxy.class.getDeclaredMethod("defineClass0", java.lang.ClassLoader.class, String.class, byte[].class, int.class, int.class);
// 修改方法的访问权限
defineClass0.setAccessible(true);
// 反射调用 java.lang.reflect.Proxy.defineClass0()方法
// 动态向JVM注册对象
// JDK8: private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);
Class<?> clazz = (Class<?>) defineClass0.invoke(null, systemClassLoader, className, classBytes, 0, classBytes.length);
return clazz;
}
}
TransletClassLoader
ClassLoader 的 defineClass 方法只能通过反射调用,在实际环境中很难有利用场景。
但是在 TemplatesImpl 类中有一个内部类 TransletClassLoader 它重写了 defineClass,并且这里没有显式地声明其定义域。Java 中默认情况下,如果一个方法没有显式声明作用域,其作用域为 default。所以也就是说这里的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。
但是 TransletClassLoader 是内部类,只允许 TemplatesImpl 类中的方法调用,利用链如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 利用链1。直接使用无效,因为没有创建的新实例。
// 因为 ClassLoader 只会将字节码文件加载到 JVM 中,不会执行 static 中的内容,只有在 newInstance 才会去执行 static 代码块。
// 所以需要使用 Class.forName("org.example.Test"); 来加载 static 修饰的内容,或者使用 newTransformer() 方法实例化 TemplatesImpl 类。
TemplatesImpl#getTransletIndex() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass
// 利用链2
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
// 利用链3(与利用链2基本重合了)
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类。
构造的特殊类:
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
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class Test extends AbstractTranslet {
static {
System.out.println("TransletClassLoader Test");
}
public Test() {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
构造简单的 POC:
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
/**
* 使用反射修改 TemplatesImpl 类的成员变量方式触发命令执行,Jackson 和 Fastjson 采用这种方式触发RCE
*
* TemplatesImpl 中 _bytecodes 成员变量,用于存储类字节码,通过 JSON 反序列化的方式可以修改该变量值,但因为该成员变量没有可映射的 get/set 方法所以需要修改 JSON 库的虚拟化配置,比如 Fastjson 解析时必须启用Feature.SupportNonPublicField、Jackson 必须开启JacksonPolymorphicDeserialization(调用mapper.enableDefaultTyping()),所以利用条件相对较高。
*
* @throws Exception 调用异常
*/
public static void main(String[] args) throws Exception {
String filepath = "C:\\Test.class";
byte[] evilBytes = Files.readAllBytes(Paths.get(filepath));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{evilBytes});
setFieldValue(templates, "_name", "org.example.Test");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 利用链 1
// templates.getTransletIndex();
// Class.forName("org.example.Test");
// 利用链 2
templates.newTransformer();
// 利用链 3
// templates.getOutputProperties();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
在 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类中:
_bytecodes
属性,用来存储类字节码_tfactory
属性,对该模板对象所属的转换器工厂的引用(需要一个 TransformerFactoryImpl 对象,因为TemplatesImpl#defineTransletClasses()
方法里有调用到_tfactory.getExternalExtensionsMap()
,如果是 null 会出错。)_name
属性,主类的名称,如果未知则为默认名称(可以是任意字符串,只要不为 null 即可)