文章

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 规范定义了如下类型的类的加载器:

  1. 系统类加载器 (App ClassLoader) 默认的类加载器
  2. 扩展类加载器 (Extension ClassLoader)
  3. 引导类加载器 (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 的核心方法有:

  1. loadClass (加载指定的 Java 类)
  2. findClass (查找指定的 Java 类)
  3. findLoadedClass (查找 JVM 已经加载过的类)
  4. defineClass (定义一个 Java 类)
  5. 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.ClassLoaderjava.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 即可)
本文由作者按照 CC BY 4.0 进行授权

© h0ny. 保留部分权利。

本站由 Jekyll 生成,采用 Chirpy 主题。