Java反射详解

一、反射概述

反射就是把Java类中的各个成分映射成一个个的Java对象。即在运行状态中,对于任意一个类,都能够知道这个类所有的属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性。这种动态获取信息及动态调用对象方法的功能叫Java的反射机制。

 

二、Java类加载机制

在Java程序启动时,JVM会将一部分类(class文件)先加载(并不是所有的类都会在一开始加载),通过ClassLoader将类加载,在加载过程中,会将类的信息提取出来(存放在元空间中,JDK1.8之前存放在永久代),同时也会生成一个Class对象存放在内存(堆内存),注意此Class对象只会存在一个,与加载的类唯一对应!

如果我们手动创建一个与JDK包名一样,同时类名也保持一致,那么JVM会加载这个类吗?

实际上我们的类最开始是由BootstarpClassLoader进行加载,BootstarpClassLoader用于加载JDK提供的类,而我们自己编写的类实际上是AppClassLoader,只有BootstarpClassLoader都没有加载的类,才会让AppClassLoader来加载,因此我们自己编写的同名包同名类不会被加载,而实际要去启动的是真正的类。

通过下面的程序可以更好的理解,由于BootstarpClassLoader是C++编写的,所以打印出来的结果是null。

 

三、反射实例

1、获取Class对象的三种方式

1)每个类(包括包装类)都有对应的Class对象,可以通过class关键字获取

2)使用Class类静态方法forName(Sting className),通过包名.类名获取,注意返回值是Class<?>

3)Object类中的getClass方法

通过比较验证了一开始的结论,在JVM中每个类始终只存在一个Class对象,无论通过什么方法获取,都是一样的。

既然拿到了类的定义,那么是否可以通过Class对象来创建对象、调用方法、修改变量呢?

2、创建类对象

1)通过使用newInstance()方法来创建对应类型的实例

public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    Class<?> clazz = Class.forName("com.kinyoobi.Reflect.Student");  //获取对应的Class对象
    Student student = (Student) clazz.newInstance();  //通过反射创建了对象
    student.test();
}


public class Student {
    public Student() {
    }
    public void test(){
        System.out.println("HxH");
    }
}

注意newInstance()只适用于默认无参构造。

  • 当类默认的构造方法被带参构造覆盖时,会出现InstantiationException异常
  • 当默认无参构造的权限不是public时,会出现IllegalAccessException异常

2)通过获取构造器getConstructor()来实例化对象

getConstructor(Class<?>... parameterTypes)的作用是根据参数类型(可变参数)来获取公共的构造器Constructor[](public)。通过这个方法获取到类的构造方法后,使用newInstance填入参数即可实例化对象。

public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
        Class<?> clazz = Class.forName("com.kinyoobi.Reflect.Student");  //获取对应的Class对象
        Student student = (Student) clazz.getConstructor(String.class).newInstance("Togashi");
        student.test();
    }


public class Student {
    public Student(String name) {
    }
    public void test(){
        System.out.println("HxH");
    }
}

当访问权限不足时,会无法找到此构造方法(NoSuchMethodException)。

使用getDeclaredConstructor()方法可以找到类中的所有构造方法。在修改访问权限后,就可以使用非public方法了。

public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
        Class<?> clazz = Class.forName("com.kinyoobi.Reflect.Student");  //获取对应的Class对象
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);   //修改访问权限
        Student student = (Student) constructor.newInstance("Togashi");
        student.test();
    }


public class Student {
    private Student(String name) {
    }
    public void test(){
        System.out.println("HxH");
    }
}

这意味着,反射可以无视权限修饰符访问类的内容

3、调用类方法

1)通过调用getMethod()方法可以获取到类中所有声明为public的方法,得到一个Method对象。当出现非public方法时,我们可以通过反射来无视权限修饰符,通过getDeclaredMethod()获取非public方法。

2)通过Method对象的invoke()方法来调用执行已经获取到的方法,注意传参。

下面使用一个例子更加具体的说明:

public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
        Class<?> clazz = Class.forName("com.kinyoobi.Reflect.Student");  //获取对应的Class对象
        Object student = clazz.newInstance();  //创建出学生对象
        Method method1 = clazz.getDeclaredMethod("test", String.class, int.class);   //通过方法名和形参类型获取类中的方法
        method1.setAccessible(true);  //修改访问权限
        method1.invoke(student, "masterpiece", 10000000);   //通过Method对象的invoke方法来调用方法

        Method method2 = clazz.getDeclaredMethod("write", String.class);
        method2.invoke(student, "kami");

        clazz.getMethod("shout",  null).invoke(student, null);  //返回值就是方法的返回值,因为这里是void,返回值为nul

    }


public class Student {
    private void test(String str,int i){
        System.out.println("HxH: "+str);
        System.out.println("point: "+(String.valueOf(i)));
    }

    protected String write(String str){
        System.out.println("Togashi is "+str);
        return str;
    }

    public void shout(){
        System.out.println("I want to play games!");
    }
}

运行结果如下:

总的来说,通过 method(方法名,参数类型).invoke(class,参数)可以调用运行class.method(参数)。

4、修改类的属性

通过getField()方法来获取一个类定义的指定成员字段修改一个类的对象中的成员字段值。当访问private字段时,同样可以按照上面的操作进行越权访问。

public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        Class<?> clazz = Class.forName("com.kinyoobi.Reflect.Student");  //获取对应的Class对象
        Student student = (Student) clazz.newInstance();  //创建出学生对象
        Field ageField = clazz.getField("age");   //获取类的成员字段age
        ageField.set(student, 17);   //将类实例student的成员字段age设置为17
        student.getAge();
        Field nameField = clazz.getDeclaredField("name");
        nameField.setAccessible(true);
        nameField.set(student, "kinyoobi");
        student.getName();
    }


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

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

    public int getAge() {
        System.out.println(age);
        return age;
    }

    public String getName() {
        System.out.println(name);
        return name;
    }

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

 

四、Java反射机制利用

1、forname()

forname静态方法有两种用法,如下:

//指定类名
public static Class<?> forName(String className)throws ClassNotFoundException  

//指定类名、是否初始化及指定类加载器
public static Class<?> forName(String name, boolean initialize,ClassLoader loader)throws ClassNotFoundException

第一个仅通过指定类名。

第二个可以指定类名、是否初始化及指定类加载器。这里第二个参数“是否初始化”指的是类的初始化。

参考下面的例子:

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(),然后是{},最后是构造方法。

值得注意的是:forname方法加载类时会自动初始化该类对象

也就是说,如果forname的参数可控,那么可以通过构造对应的恶意类,在恶意类的static()内编写恶意代码,这样当forname()执行时就会执行对应的恶意代码。

2、newInstance()

1)newInstance()和new的区别

  1. newInstance()是一个方法,而new是一个关键字。
  2. 创建对象的方式不一样,newInstance()是使用类加载机制 ,new是创建一个新类。
  3. 使用newInstance()方法时必须保证这个类已经加载过且已经连接了,而new创建类时这个类可以没有被加载。
  4. newInstance()只适用于默认无参构造。

2)在写漏洞利用方法的时候最常见的一个例子:

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

因为Runtime的构造方法是私有的,newInstance()无法将其实例化,只能通过Runtime.getRuntime()来获取Runtime对象。

修改后的payload如下:

Class clazz = Class.forName("java.lang.Runtime");
//通过clazz.getMethod("getRuntime").invoke(clazz)实例化对象
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe"); 

3、getConstructor()

如果一个类中没有无参构造方法,也没有静态的获取对象方法,即当newinstance()和method().invoke()都用不上了,不妨考虑用获取构造器getConstructor()来实例化对象。

例如下面的例子:

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

也相当于写成:

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

 

Reference:

https://www.cnblogs.com/v1ntlyn/p/13549914.html

https://blog.csdn.net/sinat_38259539/article/details/71799078

https://blog.csdn.net/qq_25928447/article/details/120962880?spm=1001.2014.3001.5502

 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
posted @ 2022-05-31 23:46  kinyoobi  阅读(539)  评论(0编辑  收藏  举报