Java安全-反射与命令执行(1)

21次阅读
没有评论

共计 5501 个字符,预计需要花费 14 分钟才能阅读完成。

前言

Java 反射真是太伟大了哈哈哈哈哈哈
Java 安全 - 反射与命令执行(1)

为了更加系统性学习 Java,我应该会把学习笔记记录下来发到博客慢慢记录我学 Java 的过程

什么是 Java 反射?

在常规的 Java 开发中,我们必须先有类(.class 文件),然后才能在代码里通过 new 来创建对象。而 反射 的出现,本质上是为了让程序能够 在运行状态中,知道一个类的所有属性和方法,并能动态地调用它们。

这么说可能有点抽象,我们来举个例子
正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例:

// Main.java
import com.itranswarp.learnjava.Person;

public class Main {String getFullName(Person p) {return p.getFirstName() + " " + p.getLastName();}
}

那我问你,假设你需要调用一个你不知道的实例怎么办?

为了解决这种问题诞生了反射

我们用一个实际场景来演示反射的作用

  • 场景:你在开发一个图像处理软件,支持多种滤镜(黑白、复古、模糊)。
  • 需求: 用户只需要在配置文件里改个名字,程序就能自动切换滤镜,而 不需要修改主程序的代码,也不需要重新编译

不用反射的情况下:

public class ImageProcessor {public void applyFilter(String filterName) {
        // 如果没有反射,你必须预知所有的滤镜类
        if (filterName.equals("BlackWhite")) {new BlackWhiteFilter().execute();} else if (filterName.equals("Retro")) {new RetroFilter().execute();} else {System.out.println("未知滤镜");
        }
    }
}

你用了一堆 if-else 写死情况

使用反射:

import java.lang.reflect.Method;

public class ImageProcessor {public void applyFilter(String className) {
        try {
            // 1. 根据字符串动态加载类(即便这个类现在还没写出来)Class clazz = Class.forName(className);

            // 2. 动态创建对象
            Object filterInstance = clazz.getDeclaredConstructor().newInstance();

            // 3. 动态获取并执行方法
            Method method = clazz.getMethod("execute");
            method.invoke(filterInstance);

        } catch (Exception e) {System.out.println("滤镜加载失败:" + e.getMessage());
        }
    }
}

第一种方法叫正射,通过 new 关键字来直接初始化新建实例。
第二种方法就是反射,动态生成类。

通过反射调用方法

上面的演示代码中有四个十分重要的方法

  • 获取类的⽅方法:forName
  • 实例例化类对象的⽅方法:newInstance
  • 获取函数的⽅方法:getMethod
  • 执⾏行行函数的⽅方法:invoke
    调用非静态方法的时候都必须要传入一个实例

刚刚的演示我们使用的是 forName 来获取 Class 类,除此之外还有 getClass() 方法或者已经加载了这个类的话还可以用 Test.Class 来获取这个类

package org.example;  

public class getclz {public static void main(String[] args) {  
        try {  
            String abc = "abc";  
            Class clz1 = abc.getClass();  
            Class clz2 = String.class;  
            Class clz3 = Class.forName("java.lang.String");  
            System.out.println(clz1 == clz2);  
            System.out.println(clz1 == clz3);  
            System.out.println(clz1);  
            System.out.println(clz2);  
            System.out.println(clz3);  
        }catch (Exception e){}}  
}
/*
true
true
class java.lang.String
class java.lang.String
class java.lang.String
*/

反射在 Java 安全中是十分重要的,我们后面学到的 Java 反序列化以及命令执行等都需要用到 Java 反射

通过 Java 反射进行命令执行

假设我们现在有一个 String 的对象, 如何利用反射实现命令执行?

package org.example;  

public class getclz {public static void main(String[] args) {  
        try {  
            String abc = "abc";  
            System.out.println(abc.getClass().getClass().forName("java.lang.Runtime").getMethod("exec", String.class).invoke(abc.getClass().getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null), "calc"  
                    )  
            );  

        } catch (Exception e) {}}  
}

总的来说我们反正要先获取 class 类,实例化对象然后调用 exec 方法(传入获取的对象和命令)
在上面的 payload 中我们使用了 abc.getClass().getClass() 获取 class 类,再使用 forName 与 getMethod 成功获取 Runtime 类以及调用 exec 方法,但是在 Java 中除了静态方法都需要传入实例对象作为参数。
所以 exec 的第一个参数是通过 abc.getClass().getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null) 获取的程序的实例,然后第二个参数才是我们要执行的命令

ClassLoader 机制

之前我们用到了 forName()方法获取我们想要的类,但翻一下 Java 开发手册就知道,forName 还有一个三参数的重载方法
Java 安全 - 反射与命令执行(1)
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

其中有三个参数

1. String name(类全限定名)

这是最基础的参数,代表你想要加载的类的名字。

  • 格式要求 :必须是 全限定名(Fully Qualified Name),例如 java.lang.Stringcom.mysql.cj.jdbc.Driver
  • 注意:如果是内部类,需要使用 $ 符号。例如 OuterClass$InnerClass

2. boolean initialize(是否初始化)

这个参数决定了类在加载后,是否立即执行 初始化(Initialization)
在 Java 类加载的过程中,主要分为:加载 -> 链接(验证、准备、解析)-> 初始化

  • true(默认值):加载类后,立即执行类的 静态代码块 static {})并为 静态变量 赋初始值。
  • false:只将类加载到 JVM 中并完成链接,但不执行静态代码块。

为什么这个参数很重要?

以 JDBC 驱动加载为例:当调用 Class.forName("com.mysql.cj.jdbc.Driver") 时,默认 initializetrue,这会触发驱动类里的静态代码块,从而自动向 DriverManager 注册自己。

3. ClassLoader loader(类加载器)

这个参数指定使用哪个类加载器来加载该类。

  • 作用 :在 Java 中,类的唯一性是由 类全限定名 + 类加载器 共同决定的。同一个类文件如果被不同的类加载器加载,在 JVM 中会被视为两个不同的类。
  • 常见选择

    • Thread.currentThread().getContextClassLoader():当前线程上下文加载器(最常用)。
    • TargetClass.class.getClassLoader():使用某个已知类的加载器。
    • null:使用根类加载器(Bootstrap ClassLoader)。

我们来结合实际来讲一下剩下两个参数的作用

initialize

public class ForNameDemo {public static void main(String[] args) throws Exception {
        String className = "SecretClass";

        System.out.println("--- 场景 A: initialize = false ---");
        // 只加载类,不初始化
        Class<?> clazz1 = Class.forName(className, false, ForNameDemo.class.getClassLoader());
        System.out.println("类已加载:" + clazz1.getName());

        System.out.println("n--- 场景 B: initialize = true ---");
        // 加载并初始化
        Class<?> clazz2 = Class.forName(className, true, ForNameDemo.class.getClassLoader());
        System.out.println("类已加载:" + clazz2.getName());
    }
}

class SecretClass {
    static {
        // 如果这个块运行了,说明类被“初始化”了
        System.out.println("【警报】SecretClass 的静态代码块被执行了!");
    }
}
/*
--- 场景 A: initialize = false ---
类已加载: SecretClass

--- 场景 B: initialize = true ---【警报】SecretClass 的静态代码块被执行了!类已加载: SecretClass
*/

ClassLoader

//LoaderDemo.java
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class LoaderDemo {public static void main(String[] args) throws Exception {
        String className = "SecretPayload";
        // 1. 获取当前标准的 AppClassLoader
        ClassLoader appLoader = LoaderDemo.class.getClassLoader();

        // 2. 创建一个自定义加载器,指向我们存放 class 的秘密目录
        File file = new File("D:/test/"); // 这里改为你存放 class 的实际路径
        URL[] urls = new URL[]{file.toURI().toURL()};
        ClassLoader customLoader = new URLClassLoader(urls);

        System.out.println("--- 场景 A:使用默认加载器 ---");
        try {Class.forName(className, true, appLoader);
        } catch (ClassNotFoundException e) {System.err.println("失败:默认加载器找不到这个类,因为它不在项目的 Classpath 里。");
        }

        System.out.println("n--- 场景 B:使用自定义加载器 (模拟 RCE) ---");
        try {
            // 这里我们传入了 customLoader
            Class<?> clazz = Class.forName(className, true, customLoader);
            System.out.println("成功:使用自定义加载器找到了类" + clazz.getName());
        } catch (ClassNotFoundException e) {System.err.println("失败:路径配置错误,请检查 D:/test/ 是否有 SecretPayload.class");
        }
    }
}
//SecretPayload.java
public class SecretPayload {
    static {System.out.println("n[!!!] 远程代码执行成功!");
        System.out.println("加载器 ID:" + SecretPayload.class.getClassLoader());
    }
}

将 SecretPayload.java 编译成 class 后移动到目标目录(不要在 classpath 或者和 LoaderDemo.class 在同一目录)
执行后会有

/*
--- 场景 A:使用默认加载器 ---
失败:默认加载器找不到这个类,因为它不在项目的 Classpath 里。--- 场景 B:使用自定义加载器 (模拟 RCE) ---

[!!!] 远程代码执行成功!加载器 ID: java.net.URLClassLoader@626b2d4a
成功:使用自定义加载器找到了类 SecretPayload
*/

ClassLoader 这里有很多的利用空间,这里暂时不做过多讲解。

正文完
 0
Rycarl
版权声明:本站原创文章,由 Rycarl 于2026-04-23发表,共计5501字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码