Java反射机制

Java学习

Java反射机制

Java反射(Reflection)是Java非常重要的动态特性**,通过使用反射我们可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java实例、调用任意的类方法、修改任意的类成员变量值等**。

这是一个重要的机制,可以绕过java私有访问权限检查,反射获取并调用私有的类从而可以进行命令执行

本地JDK测试版本:

  • JDK 8u71

最好是搭建一个maven项目来学习,这样更好添加依赖。

反射机制流程

image-20250120193740480

当我们创建了一个类文件,经过javac编译之后,就会形成.class文件,同时JVM内存会查找生成的.class文件读入内存和经过ClassLoader加载,同时会自动创建生成一个Class对象,里面拥有其获取的成员变量,成员方法,和构造方法等。

JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息。获取了某个Class实例,就可以通过这个Class实例获取到该实例对应的class的所有信息

反射常用的包和类

反射机制相关操作一般位于 java.lang.reflect 包中

需要注意的类:

1
2
3
4
java.lang.Class:类对象
java.lang.reflect.Constructor:类的构造器对象
java.lang.reflect.Field:类的属性对象
java.lang.reflect.Method:类的方法对象

反射常见使用的方法

  • 获取类的方法:forName

  • 实例化类对象的方法:newInstance

  • 获取函数的方法:getMethod

  • 执行函数的方法:invoke

我们可以使用这些方法来获得其他类的各种属性和方法

获取class对象

forName不是获取“类”的唯一途径,通常来说还有下面三种利用java.lang.Class对象的方式来获取一个“类”

获取Class对象的三种方式:

  • Class.forName(“全类名”) :将字节码文件加载进内存,返回Class对象

多用于配置文件,将类名定义在配置文件中。读取文件,加载类。

  • 类名.class :通过类名的属性class获取

多用于参数的传递

  • 对象.getClass() :getClass()方法定义于Object类中,并且需要先实例化一个对象。这里有一个点要说明。如果不是一个实例化对象,而是一个class对象,那么都会返回class java.lang.Class,同样的,如果是一个方法的class对象,那么返回的会是class java.lang.reflect.Method,注意区分辨别。
  • classloader.loadClass():这是通过类加载器来加载类,这个板块不说,后面到类加载器再了解。

多用于对象的获取字节码的方式

—-————

给一个代码来演示一下获取Class对象的三种方式:

目录结构为:

image-20241217185654303

  • demo:Reflection.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package java_foundation;

public class Reflection {
    private String name;
    protected String sex;
    public int age = 222;

    public int getAge(){
        return this.age;
    }

    public void setName(String name){
        this.name=name;
    }

    public String getName(){
        return this.name;
    }
}
  • 执行代码效果:Main.java:
 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 java_foundation;

import java.lang.Class;

public class Main {
    public static void main(String[] args) throws Exception{
        //方法一:
        Class clazz = Class.forName("java_foundation.Reflection");
        Reflection reflection = (Reflection)clazz.newInstance();//实例化对象
        reflection.setName("fupanc");
        System.out.println(reflection.getName());
        //方法二
        Class clazz1 = Reflection.class;
        Reflection reflection1 = (Reflection)clazz1.newInstance();
        reflection1.setName("fupanc1");
        System.out.println(reflection1.getName());
        //方法三
        Reflection reflection2 = new Reflection();
        Class clazz2 = reflection2.getClass();
        Reflection reflection3 = (Reflection)clazz2.newInstance();
        System.out.println(reflection3.getAge());
        //其他说明
        Class reflection5 = Reflection.class;
        System.out.println(reflection5.getClass());
        System.out.println(reflection5.getName());
    }
}

输出为:

1
2
3
4
5
fupanc
fupanc1
222
class java.lang.Class
java_foundation.Reflection

可以清楚明白地看出这里的差别,重要的是,学习java一定要有对象的概念。

需要注意的是,在获取class对象中,一般使用Class.forName方法去获取,其他两个都有一定的限制。

另外的一个点需要说明

forName有两个函数重载:

  • Class<?> forName(String name)

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

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

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

默认情况下, forName 的第⼀个参数是类名;第⼆个参数表示是否初始化;第三个参数就是 ClassLoader 。

对于第二个参数,可以将其理解为对一个类的初始化,构造函数并不会执行,看如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package java_foundation;

import java.lang.Class;

public class Main {
    public static void main(String[] args) throws Exception{
        Class clazz = Class.forName("java_foundation.Test");
    }
}
class Test{
    {
        System.out.println("直接调用的{}");
    }
    static{
        System.out.println("直接调用的static");
    }
    public Test(){
        System.out.println("调用了类的构造函数");
    }
}

输出结果为:

1
直接调用的static

这样就可以很容易地看出“类的初始化"调用的是static {},然后当我尝试Test test = new Test();这样显式地初始化时,可以发现调用顺序是:static{} ==》{} ==》构造函数。

可以延伸一下,假设我们有如下函数,其中函数的参数name可控:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package java_foundation;

import java.lang.Class;

public class Main {
    public static void main(String[] args) throws Exception{
        String name ="java_foundation.MyTest";
        Class clazz = Class.forName(name);
    }
}

我们就可以编写⼀个恶意类,将恶意代码放置在 static {} 中,从⽽执⾏:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package java_foundation;
import java.lang.Runtime;
import java.lang.Process;

public class MyTest {
    static{
        try{
            Runtime runtime = Runtime.getRuntime();
            String[] commands = {"calc"};
            Process pc = runtime.exec(commands);
            pc.waitFor();
        }catch(Exception e){

        }
    }
}

这样是可以成功弹计算机的,当然调用不一定是在main主方法中,还可以是其他方法内部调用。

获取成员变量Field

获取成员变量Field,位于java.lang.reflect.Field中,常使用的方法有如下几种

  • Field[] getFields():获取所有public修饰的成员变量(包括父类)
  • Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符(不包括父类)
  • Field getField(String name):获取指定名称的public修饰的成员变量(包括父类)
  • Field getDeclaredField(String name):获取指定的成员变量(不包括父类)

获取字段

还是利用之前那个demo来演示一下

Reflection.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package java_foundation;

public class Reflection {
    private String name;
    protected String sex;
    public int age = 222;

    public int getAge(){
        return this.age;
    }

    public void setName(String name){
        this.name=name;
    }

    public String getName(){
        return this.name;
    }
}

Main.java:

 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
package java_foundation;

import java.lang.Class;
import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception{
        String name ="java_foundation.Reflection";
        Class clazz = Class.forName(name);
        Field[] field = clazz.getDeclaredFields();
        for(Field x : field){
            System.out.println(x);
        }
        System.out.println("==============");
        Field field1 = clazz.getDeclaredField("sex");
        System.out.println(field1);
        System.out.println(field1.getName());
        System.out.println(field1.getType());
        System.out.println("==============");
        Field field2 = clazz.getDeclaredField("age");
        System.out.println(field2);
        System.out.println(field2.getName());
        System.out.println(field2.getType());
    }
}
//对于数组类型的需要使用for循环来便利输出

输出为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private java.lang.String java_foundation.Reflection.name
protected java.lang.String java_foundation.Reflection.sex
public int java_foundation.Reflection.age
==============
protected java.lang.String java_foundation.Reflection.sex
sex
class java.lang.String
==============
public int java_foundation.Reflection.age
age
int

我们还需要了解的是一个Field对象包含了一个字段的所有信息,可以使用如下函数获取

  • getName():返回字段名称
  • getType():返回字段类型,也是一个Class实例
  • getModifiers():返回字段修饰符
  • get(obj):获取字段值
  • set:修改字段值

这时再看上面给的代码,就可以知道不使用getName()直接输出就可以获取到变量的修饰符以及类型。

获取字段值

这里就需要用到get(obj),直接看代码。

这里将Reflection.java稍微改改

1
2
3
4
5
6
7
package java_foundation;

public class Reflection {
    private String name = "fupanc";
    protected String sex = "boy";
    public int age = 222;
}

Main.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package java_foundation;

import java.lang.Class;
import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception{
        String name ="java_foundation.Reflection";
        Class clazz = Class.forName(name);

        System.out.println("==============");
        Field field1 = clazz.getDeclaredField("sex");
        System.out.println(field1.get(clazz.newInstance()));
        System.out.println("==============");
        Field field2 = clazz.getDeclaredField("age");
        System.out.println(field2.get(clazz.newInstance()));
        System.out.println("==============");
        Field field3 = clazz.getDeclaredField("name");
        System.out.println(field3.get(clazz.newInstance()));
    }
}

输出为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
==============
boy
==============
222
==============
Exception in thread "main" java.lang.IllegalAccessException: Class java_foundation.Main can not access a member of class java_foundation.Reflection with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
	at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
	at java.lang.reflect.Field.get(Field.java:390)
	at java_foundation.Main.main(Main.java:19)

这里可以看到private字段抛出错误,可以调用Field.setAccessible(true),更改代码如下:

 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
package java_foundation;

import java.lang.Class;
import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception{
        String name ="java_foundation.Reflection";
        Class clazz = Class.forName(name);

        System.out.println("==============");
        Field field1 = clazz.getDeclaredField("sex");
        System.out.println(field1.get(clazz.newInstance()));
        System.out.println("==============");
        Field field2 = clazz.getDeclaredField("age");
        System.out.println(field2.get(clazz.newInstance()));
        System.out.println("==============");
        Field field3 = clazz.getDeclaredField("name");
        field3.setAccessible(true);
        System.out.println(field3.get(clazz.newInstance()));
    }
}
/*
==============
boy
==============
222
==============
fupanc
*/

修改字段值

基本格式:

1
2
Field f = stdClass.getDeclaredField("grade");
f.set(obj, "xxxx");

这样基本就懂了,这里的obj就是一个实例,需要注意:对于private修饰的字段修改方法,同样需要调用Field.setAccessible(true)来使其可访问。测试代码如下: Main.java:

 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
package java_foundation;

import java.lang.Class;
import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception{
        String name ="java_foundation.Reflection";
        Class clazz = Class.forName(name);

        Object o = clazz.newInstance();//实例化对象
        System.out.println("==============");
        Field field1 = clazz.getDeclaredField("sex");
        System.out.println(field1.get(o));
        System.out.println("修改后的内容:");
        field1.set(o,"girl");
        System.out.println(field1.get(o));
        System.out.println("==============");
        Field field2 = clazz.getDeclaredField("age");
        System.out.println(field2.get(o));
        System.out.println("修改后的内容:");
        field2.set(o,1111);
        System.out.println(field2.get(o));
        System.out.println("==============");
        Field field3 = clazz.getDeclaredField("name");
        field3.setAccessible(true);//注意反射还是需要调用这个
        System.out.println(field3.get(o));
        System.out.println("修改后的内容:");
        field3.set(o,"hahaha");
        System.out.println(field3.get(o));
    }
}

输出为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
==============
boy
修改后的内容:
girl
==============
222
修改后的内容:
1111
==============
fupanc
修改后的内容:
hahaha

注意看代码,这里为什么要实例化一个对象赋值给一个变量,而不是像上面获取字段值那样直接用ctf.newInstance()直接来代表obj,可以先自己想想😈,现在给出解释:

重点其实还是理解代码,这里调用了两次o,也就是调用了两次对象。如果我们两个地方都使用ctf.newInstance(),那么就会实例化两次,也就是会让前面输出和后面修改的对象是不一致的。所以需要使用一个o来代表这是同一个对象,从而输出修改后的结果。不要小看这一个小小的问题哦

修改final关键字修饰的成员变量

这里单独拿出来说是因为final关键字修饰的特性,被final关键字修饰的变量表明其数值在初始化之后就不能再更改,并且需要在定义这个字段时就声明值,并且是不能通过设置setter方法来修改,如下代码说明:

1
2
3
4
5
6
class Something{
    private final String name = "fupanc";
    public void setName(String name){
        this.name = name;
    }
}

此时就会报错: 无法将值赋给 final 变量 ’name’。

然后按照我的理解,其实前面的set()和get()方法其本质就是setter和getter方法。所以这里该怎么修改呢?

按照下面套就行,注意需要导入java.lang.reflect.Modifier

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 反射获取Field类的modifiers
Field modifiers = field.getClass().getDeclaredField("modifiers");

// 设置modifiers修改权限
modifiers.setAccessible(true);

// 修改成员变量的Field对象的modifiers值(像是移除final修饰符)
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

// 修改成员变量值
field.set(类实例对象, 修改后的值);

实例使用如下:

 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
package java_foundation;

import java.lang.reflect.Modifier;
import java.lang.reflect.Field;

public class Text {
    public static void main(String[] args) throws Exception {
        Something o = new Something();
        Class clazz = Class.forName("java_foundation.Something");
        Field field = clazz.getDeclaredField("name");
        field.setAccessible(true);
        System.out.println(field.get(o));
        Field modifiers = field.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(field,field.getModifiers() & ~Modifier.FINAL);
        field.set(o,"hahaha");
        System.out.println(field.get(o));
    }
}
class Something{
    private final String name = "fupanc";
    public String getName(){
        return this.name;
    }
}
/*
fupanc
hahaha
*/

这样就可以修改User类中的private和final属性的name值

获取类的方法

想要创建Method需要导包,位于java.lang.reflect.Method下,常使用的方法如下:

1
2
3
4
5
Method getMethod(name,Class...)获取某个public的Method包括父类
Method getDeclaredMethod(name,Class...)获取当前类的某个Method不包括父类
//第一个参数获取该方法的名字,第二个参数获取标识该方法的参数类型
Method[] getMethods()获取所有public的Method包括父类
Method[] getDeclaredMethods()获取当前类的所有Method不包括父类

同样的一个Method对象包含一个方法的所有信息:

  • getName():返回方法名称
  • getReturnType():返回方法返回值类型,也是一个Class实例
  • getParameterTypes():返回方法的参数类型,是一个Class数组
  • getModifiers():返回方法的修饰符

获取方法

直接给代码看如何利用,为了凸显结果方便理解,这里改一下Reflection.java的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package java_foundation;

public class Reflection {
    private String name = "fupanc";
    protected String sex = "boy";
    public int age = 222;

    public int getAge(){
        return this.age;
    }
    private void setAge(int age){
        this.age = age;
    }

    protected void setName(String name){
        this.name=name;
    }

    public String getName(){
        return this.name;
    }
}

Main.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package java_foundation;

import java.lang.Class;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception{
        String name ="java_foundation.Reflection";
        Class clazz = Class.forName(name);

        Method[] method = clazz.getDeclaredMethods();
        for(Method x:method){
            System.out.println(x);
        }
        System.out.println("==============");
        Method method1 = clazz.getDeclaredMethod("setAge",int.class);
        System.out.println(method1.getName());
        System.out.println(method1.getReturnType());
    }
}

输出为:

1
2
3
4
5
6
7
public java.lang.String java_foundation.Reflection.getName()
protected void java_foundation.Reflection.setName(java.lang.String)
private void java_foundation.Reflection.setAge(int)
public int java_foundation.Reflection.getAge()
==============
setAge
void

这里需要注意的就是对于不同修饰符修饰的方法在获取时使用的方法的不同,但其实一般都是使用的Declared那类方法。

调用方法

invoke():调用方法。

invoke的作用是执行方法,需要两个参数,第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类

第二个参数是这个方法的需要传入的参数。

这里需要注意一点,在调用方法这里与获取成员变量差不多,private修饰符,需要通过Method.setAccessible(true)允许其调用。

先简单给代码看看

Reflection.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package java_foundation;

public class Reflection {
    private String name = "fupanc";
    protected String sex = "boy";
    public int age = 222;

    public int getAge(){
        return this.age;
    }
    private void setAge(int age){
        this.age = age;
    }

    protected void setName(String name){
        this.name=name;
    }

    public String getName(){
        return this.name;
    }
}

Main.java

 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
package java_foundation;

import java.lang.Class;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception{
        String name ="java_foundation.Reflection";
        Class clazz = Class.forName(name);
        Object o = clazz.newInstance();

        Method method1 = clazz.getDeclaredMethod("setAge",int.class);
        method1.setAccessible(true); //需要允许访问
        method1.invoke(o,123);

        Method method2 = clazz.getDeclaredMethod("getAge");
        System.out.println(method2.invoke(o));

        Method method3 = clazz.getDeclaredMethod("setName",String.class);
        method3.invoke(o,"hahaha");

        Method method4 = clazz.getDeclaredMethod("getName");
        System.out.println(method4.invoke(o));
    }
}
/*
123
hahaha
*/

可以看出成功调用了setName/setAge方法并设置了name/age的值,可以自己再调试一下,这里只有private修饰的方法才需要调用setAccessible(true),注意看代码之间的联系。

稍微说明一下静态方法的利用方式,如下格式:

1
2
3
4
5
6
7
8
Method f = Runtime.class.getMethod("getRuntime");
 Runtime r = (Runtime) f.invoke(Runtime.class);
r.exec("calc");
同样的可以使用
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc");
//getRuntime()方法是静态方法,这样就可以直接执行方法从而获取Runtime类的实例化对象

这是反射API知道静态方法不需要实例对象,因此invoke方法的第一个参数(对象实例)可以是null或类对象,反射API会自动处理并忽略它。注意理解上面的格式。

获取构造函数Constructor

获取构造函数Constructor,位于java.lang.reflect.Constructors中,常使用的方法有:

  • Constructor[] getConstructors():只返回public构造函数
  • Constructor[] getDeclaredConstructors():返回所有构造函数
  • Constructor getConstructor(Class…):匹配和参数配型相符合的public构造函数
  • Constructor getDeclaredConstructor(Class…):匹配和参数配型相符的构造函数

Reflection.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package java_foundation;

public class Reflection {
    private String name;
    public int age;

    public Reflection(){
        System.out.println("调用无参构造函数");
    }
    public Reflection(String name){
        System.out.println("调用有参构造函数"+name);
    }
    private Reflection(int age){
        this.age=age;
        System.out.println("调用私有构造函数"+age);
    }
    public int getAge(){
        return this.age;
    }
}

Main.java:

 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
package java_foundation;

import java.lang.Class;
import java.lang.reflect.Constructor;

public class Main {
    public static void main(String[] args) throws Exception{
        String name ="java_foundation.Reflection";
        Class clazz = Class.forName(name);

        Constructor[] constructor = clazz.getDeclaredConstructors();
        for(Constructor x:constructor){
            System.out.println(x);
        }
        System.out.println("===============");
        Constructor constructor1 = clazz.getDeclaredConstructor(String.class);
        System.out.println(constructor1);
        //简单利用,实例化对象:
        constructor1.newInstance("fupanc");

        System.out.println("===============");
        Constructor constructor2 = clazz.getDeclaredConstructor(int.class);
        System.out.println(constructor2);
        constructor2.setAccessible(true);
        Reflection o = (Reflection)constructor2.newInstance(12345);
        System.out.println(o.getAge());
    }
}

输出为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private java_foundation.Reflection(int)
public java_foundation.Reflection(java.lang.String)
public java_foundation.Reflection()
===============
public java_foundation.Reflection(java.lang.String)
调用有参构造函数fupanc
===============
private java_foundation.Reflection(int)
调用私有构造函数12345
12345

调用private修饰的Constructor时,必须首先通过setAccessible(true)设置允许访问。

获取继承关系

获取父类

1
Class.getSuperclass()

获取interface

1
Class.getInterface()

反射创建类对象

其实这个已经在前面代码的示例中已经利用过滤,只要把前面代码看懂这个板块就没有问题。直接给代码:

1
2
Class ctf = Class.forName("Main"); // 创建Class对象
Main o =  (Main)ctf.newInstance(); // 创建类对象

则o就是对应的类对象。

这里调用的这个类的无参构造函数,但是不一定可以使用成功,原因可能为:

  • 使用的类没有无参构造函数
  • 使用的类构造函数是私有的

注意如果没有无参构造方法,可以使用Constructor.newInstance()来实例化一个类对象。如下:

1
2
3
4
5
//public Main(String name)作为构造函数:
 
Class clazz = Class.forName("Main");   clazz.getMethod("setName",String.class).invoke(clazz.getConstructor(String.class).newInstance("admin"),"haha");

//当没有权限访问时可以调用constructor.setAccessible(true)来创建出类实例

利用反射进行命令执行

利用Runtime类

java.lang.Runtime 中有一个exec方法可以执行本地命令,但是不能如下直接构造来执行命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import java.lang.reflect.*;
public class Text {
    public static void main(String[] args) throws Exception{
        Class ctf = Class.forName("java.lang.Runtime");
        Method method1 = ctf.getDeclaredMethod("exec",String.class);
        method1.invoke(ctf.newInstance(),"id");
    }
}

//中间代码可以简化如下,并且不需要导包,需要注意一下:
Class ctf = Class.forName("java.lang.Runtime");
ctf.getMethod("exec", String.class).invoke(ctf.newInstance(), "id");

这样会报错

1
2
3
4
5
Exception in thread "main" java.lang.IllegalAccessException: class Text cannot access a member of class java.lang.Runtime (in module java.base) with modifiers "private"
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
	at java.base/jdk.internal.reflect.Reflection.ensureMemberAccess(Reflection.java:99)
	at java.base/java.lang.Class.newInstance(Class.java:579)
	at Text.main(Text.java:5)

原因就是Runtime类的构造方法是私有的,导致这样ctf.newInstance()直接调用是错误的。

可以看一下源码

image-20250120193818659

可以看到有一个getRuntime()方法可以获取到对象,这种设计就是“单例模式”,这里可以使用一个静态方法来获取对象,所以在这里我们只能通过Runtime.getRuntime()来获取到Runtime对象。

这里Runtime.getRuntime()是一个静态方法,所以在使用invoke执行方法时,第一个参数需要传入一个Runtime类,所以可以将代码改成如下来执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package java_foundation;

import java.lang.Runtime;

public class Main {
    public static void main(String[] args) throws Exception{
        Class clazz = Runtime.class;
        clazz.getDeclaredMethod("exec",String.class).invoke(clazz.getDeclaredMethod("getRuntime").invoke(null),"calc");
    }
}
//成功弹出计算机

还可以通过setAccessible(true)获得访问权限,全反射调用的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import java.lang.reflect.*;
public class Text{
    public static void main(String[] args) throws Exception{
        Class ctf = Class.forName("java.lang.Runtime");
        Constructor constructor1 = ctf.getDeclaredConstructor();
        constructor1.setAccessible(true);
        Object o = constructor1.newInstance();
        Method method1 = ctf.getMethod("getRuntime");
        Object x = method1.invoke(o);
        Method method2 = ctf.getMethod("exec",String.class);
        method2.invoke(x,"calc");
    }
}

最后虽然弹出警告但是成功弹出计算机

1
2
3
4
5
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by Text (file:/D:/java_text/java-1/out/production/java-1/) to constructor java.lang.Runtime()
WARNING: Please consider reporting this to the maintainers of Text
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

其实直接如下利用即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package java_foundation;

import java.lang.Class;
import java.lang.reflect.Constructor;
import java.lang.Runtime;

public class Main {
    public static void main(String[] args) throws Exception{
        Constructor constructor = Class.forName("java.lang.Runtime").getDeclaredConstructor();
        constructor.setAccessible(true);
        Runtime rt = (Runtime)constructor.newInstance();
        rt.exec("calc");
    }
}

但是上面的全利用反射来利用还是需要学习一下。

其他类与方法利用

现在直接使用Runtime.getRuntime().exec(cmd)的调用其实已经不太好找了,在这里看到了一篇文章,也是拓展了我的思路,这里我们还可以通过跟进Runtiem类的exec()方法的底层调用过程来完成RCE功能的实现。

现在还是来调试一下,看看这里的底层实现以及自己构建代码。

基本测试类:

Main.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package java_foundation;

import java.lang.Runtime;

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

运行成功弹出计算机。

调试过程

跟进代码中的exec()方法,然后打断点:

image-20241218152025804

开始调试:

然后调用了重载的exec()方法:

image-20241218152233739

又调用了重载的exec()方法:

image-20241218152312406

然后这里就实例化了一个方法来执行这个过程。在这里我调试时发现其实这里直接return这个ProcessBuilder类后直接就弹出了计算机,说明其实这里就是一个可以直接进行命令执行的地方。所以这里就有一个可以利用的点,直接根据这里的点来构造一个简单的payload,第一构造点了,但是后面再说明。这里继续跟代码:

然后就是实例化了ProcessBuilder类:

image-20241218154021500

可以看出来这个command是一个数组类型的值,并且这里add过后的值也可以调试看一下:

image-20241218154115733

。继续往后面走,调用了environment()方法:

image-20241218154308479

这里其实就是直接返回的前面的ProcessBuilder类实例,没有其他操作。

然后调用了directory()方法:

image-20241218154413881

一个简单的赋值操作,然后返回了ProcessBuilder类实例。

最后调用了start()方法:

image-20241218194347551

代码逻辑简单跟一下,还是能看懂,主要调用的就是下面的ProcessImpl类的start()方法,并且这里在执行这个方法后弹出计算机,所以这里也是第二个构造点。现在还是不谈,后面再具体分析。

在这里就是调用了ProcessImpl类的start()方法,这里看一下ProcessImpl类的start()方法的定义:

image-20241218194547078

就是这个static修饰符,让这个start()方法可以直接被调用。那么现在再继续跟进这里的start()方法,参数传递情况:

image-20241218194737109

简单跟了一下,其实最后调用的还是如下代码:

image-20241218194959495

调试过后发现这里也是直接实例化这个过程然后弹出计算机,这里的参数传递:

image-20241218195418365

所以这里又是第三构造方法。(感觉其实这里都是一层套一层,主要是参数的传递吧)。

再继续跟进,现在来到了ProcessImpl类的初始化过程:

image-20241218200114831

所以会进入第一个if语句,就是将allowAmbiguousCommands设置为true,以及value最终值为null,然后就退出第一个if条件,随后正好就进入了第二个if条件,这里就不细跟第二个if条件了,最后退出这个if条件,将调用create()方法:

image-20241218201621163

当跳过这个create()方法就弹出计算机,说明这里也是可以利用的,第四构造点。那么现在继续跟进这个create()方法,但是一直跟不进去,alt+shift+F7强制步入也进不了。简单跟进是对应的如下方法:

image-20241218202354331

上面也是有方法说明的,大概意思就是会使用win32函数来创建进程,这里就有点太底层了。并由由于操作系统的不同,这分析出来的主要是windows系统下,对于linux系统下的利用也就会有不同,不再往下跟了。

总结一下前面的几个构造点:

  • new ProcessBuilder(cmdarray).environment(envp).directory(dir).start()
  • ProcessImpl类的start()方法。

大概就是这两个,因为linux和windows两个操作系统的不同,在第三个构造点就开始有不同代码了,如果是打linux环境也许还有更多的paylaod,这个就到时候再具体跟吧。这里就只说明这两个方法,具体区别可以看一下参考文章。

利用ProcessBuilder

对应代码:

java.lang.ProcessBuilder类用于创建操作系统进程, 还是调试代码看需要哪些参数:

首先就是新建了ProcessBuilder类实例,重点是注意参数类型以及参数的传递,先是实例化了ProcessBuilder类:

image-20241218204119544

然后将这个command变量设置为了一个数组类型的变量。

然后调用了environment()方法,但是没啥用,其实就是将ProcessBuilder类的environment变量设置为null。

然后调用了directory()方法,同样其实就是一个将ProcessBuilder类的directory变量设置为null。

最后调用了start()方法完成命令的执行。

但是其实对于中间两个变量的设置,其实在ProcessBuilder类初始化后就已经符合条件了:

image-20241218205237702

所以其实只用获取构造器和start()方法即可。但这里要说明一个点:Java中的可变长参数,当定义函数的时候不确定参数数量的时候,可以使用...这样的语法来表示”这个函数的参数个数是可变的“。同时对于可变长参数,Java在编译时会编译成一个数组,所有说下面这两种写法在底层其实是等价的:

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

对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。

那么可以如下构造paylaod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package java_foundation;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class MyTest {
    public static void main(String[] args) throws Exception{
        String[] cmd = new String[]{"calc"};
        String className = "java.lang.ProcessBuilder";
        Class clazz = Class.forName(className);
        Constructor constructor = clazz.getDeclaredConstructor(String[].class);
        Method method = clazz.getDeclaredMethod("start",null);
        method.invoke(constructor.newInstance(cmd));
    }
}

但是报错:

1
2
3
4
5
6
Exception in thread "main" java.lang.IllegalArgumentException: argument type mismatch
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
	at java_foundation.MyTest.main(MyTest.java:13)

也算积累经验了,一直在看语法,结果问报错一下就解决了,这里的基本的大部分语法是没有错误的,主要还是可变参数的原因。即ProcessBuilder类的构造函数:

image-20241218211306707

这里就直接继承一个固定的吧,也就是说。我在反射时获取了正确的构造函数String[].class,但是在实例化时有区别,主要是因为 可变参数 (varargs)数组类型 在 Java 反射中的处理方式稍微不同。就是在调用newInstance()方法时,Java会将可变参数视为一个Object[]类型,而不是直接的String[]。所以这里需要强制类型转换为Object类型。所以正确的利用方法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//原利用思路:new ProcessBuilder(new String[]{"calc"}).start();
package java_foundation;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class MyTest {
    public static void main(String[] args) throws Exception{
        String[] cmd = new String[]{"calc"};
        String className = "java.lang.ProcessBuilder";
        Class clazz = Class.forName(className);
        Constructor constructor = clazz.getDeclaredConstructor(String[].class);
        Method method = clazz.getDeclaredMethod("start",null);
        method.invoke(constructor.newInstance((Object)cmd));
    }
}

成功弹出计算机,并且只使用反射的条件。还可以如下进行说明传参:

我们将字符串数组的类String[].class传给getConstructor即可,此时就获取到了参数为数组类型的newInstance(),在调用newInstance的时候,因为本身接收的是一个可变长参数(即一个数组),并且需要我们传给 ProcessBuilder构造器的参数的是一个List<String>类型,二者叠加为一个二维数组,最终payload如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import java.lang.reflect.*;
public class Text{
    public static void main(String[] args) throws Exception{
        Class ctf = Class.forName("java.lang.ProcessBuilder");
        Constructor constructor1 = ctf.getConstructor(String[].class);
        Object o = constructor1.newInstance(new String[][]{{"calc"}});
        Method method1 = ctf.getMethod("start");
        method1.invoke(o);
    }
}
/*中间代码精简
Class ctf = Class.forName("java.lang.ProcessBuilder");
ctf.getMethod("start").invoke(ctf.getConstructor(String[].class).newInstance(new String[][]{{"calc"}}));

说一点点代码:

  • 对于new String[][]{{"calc"}}:这里使用 new 关键字创建并初始化一个二维字符串数组,确保参数类型正确匹配 。

然后在其他文章中看到了另外一个有意思的paylaod,直接看吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package java_foundation;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Arrays;

public class MyTest {
    public static void main(String[] args) throws Exception{
        Class ctf = Class.forName("java.lang.ProcessBuilder");
        Constructor constructor1 = ctf.getConstructor(List.class);
        Object o = constructor1.newInstance(Arrays.asList("calc"));
        Method method1 = ctf.getMethod("start");
        method1.invoke(o);
    }
}
/*成功弹计算机
中间代码精炼一下就是这样就不用导lang下的包):
Class ctf = Class.forName("java.lang.ProcessBuilder");        ctf.getMethod("start").invoke(ctf.getConstructor(List.class).newInstance(Arrays.asList("calc")));

这里主要就是利用的另外一个构造函数来构造的payload:

image-20241218212916855

因为这两个构造函数其实最后都是直接作用与它的command变量,也就是我们要传入的命令,这里同样可以通过获取相应的构造函数并传入相应的值来执行命令。具体就看上面的代码了,不多赘述。

ProcessImpl#start()

对应代码:

image-20241218213400242

这里其实就是对应的ProcessBuilder类调用的start()方法,也就是前一个调用方法的更深一层但是这里对应的类是不一样的,所以可以利用。

对于这里直接调用的ProcessImpl类的start()方法,这里是因为这个类的start()方法是一个静态方法,同样直接打就行了,对应的参数传递情况:

image-20241218213745133

所以直接打就行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package java_foundation;

import java.lang.reflect.Method;
import java.util.Map;

public class MyTest {
    public static void main(String[] args) throws Exception{
        String name = "java.lang.ProcessImpl";
        String[] cmd = {"calc"};
        Method method = Class.forName(name).getDeclaredMethod("start", String[].class, Map.class,String.class, ProcessBuilder.Redirect[].class, boolean.class);
        method.invoke(null,cmd,null,null,null,false);
    }
}

报错:

1
2
3
Exception in thread "main" java.lang.NoSuchMethodException: java.lang.ProcessImpl.start([Ljava.lang.String;, java.util.Map, java.lang.String, java.lang.ProcessBuilder$Redirect, boolean)
	at java.lang.Class.getDeclaredMethod(Class.java:2130)
	at java_foundation.MyTest.main(MyTest.java:10)

虽然这个start()方法没有定义为私有类型:

image-20241218220752179

但是这个start()方法所处的ProcessImpl类是final类型的的,并且都不能直接import引入,所以需要通过setAccessible(true);来让其可以访问。

所以最终代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package java_foundation;

import java.lang.reflect.Method;
import java.util.Map;

public class MyTest {
    public static void main(String[] args) throws Exception{
        String name = "java.lang.ProcessImpl";
        String[] cmd = {"calc"};
        Method method = Class.forName(name).getDeclaredMethod("start", String[].class, Map.class,String.class, ProcessBuilder.Redirect[].class, boolean.class);
        method.setAccessible(true);
        method.invoke(null,cmd,null,null,null,false);
    }
}

成功弹出计算机。

这里只分析了通用的两个payload,对于linux环境当然还可以打其他的方式,具体看参考文章。

参考文章:

https://www.cnblogs.com/Nestar/p/17335689.html

https://xz.aliyun.com/t/12446?time__1311=GqGxRQ0%3DitqiqGN4eeT4QwqWqrvD9BjiZaoD

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计