返回介绍

3 Java 反射

发布于 2024-09-16 15:35:00 字数 7365 浏览 0 评论 0 收藏 0

3.1 Java 反射定义

对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 java 语言的反射机制。

反射是⼤多数语⾔⾥都存在的特性,对象可以通过反射获取它的类,类可以通过反射拿到所有⽅法(包括私有),拿到的⽅法可以直接调用。总之,通过反射,可以将 Java 这种静态语⾔附加上 动态特性

Java 语言虽然不像 PHP 那样存在许多灵活的 动态特性 ,但是通过反射,可以达到一定的效果,如下面这段代码,在传入参数值不确定的情况下,该函数的具体作用是未知的。

public void execute(String className, String methodName) throws Exception {
    Class clazz = Class.forName(className);
    clazz.getMethod(methodName).invoke(clazz.newInstance());
}

在 Java 中定义的一个类本身也是一个对象,即 java.lang.Class 类的实例,这个实例称为类对象

  • 类对象表示正在运行的 Java 应用程序中的类和接口
  • 类对象没有公共构造方法,由 Java 虚拟机自动构造
  • 类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法

要得到类的方法和属性,首先就要得到该类对象

3.2 获取类对象

假设现在有一个 Person 类:

public class Person implements Serializable {
    private String name;
    private Integer age;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }

}

要获取该类对象一般有三种方法:

  • class.forName("com.geekby.Person")
  • Person.class
  • new Person().getClass()

最常用的是第一种,通过一个字符串即类的全路径名就可以得到类对象。

3.3 利用类对象创建对象

与直接 new 创建对象不同,反射是先拿到类对象,然后通过类对象获取构造器对象,再通过构造器对象创建一个对象。

package com.geekby;

import java.lang.reflect.*;

public class CreateObject {
    public static void main(String[] args) throws Exception {
        Class PersonClass = Class.forName("com.geekby.Person");
        Constructor constructor = PersonClass.getConstructor(String.class, Integer.class);
        Person p = (Person)constructor.newInstance("Geekby", 24);

        System.out.println(p.getName());
    }
}
方法说明
getConstructor(Class… parameterTypes)获得该类中与参数类型匹配的 公有 构造方法
getConstructors()获得该类的所有公有构造方法
getDeclaredConstructor(Class… parameterTypes)获得该类中与参数类型匹配的构造方法
getDeclaredConstructors()获得该类所有构造方法

3.4 利用反射调用方法

public class CallMethod {
    public static void main(String[] args) throws Exception {
        Class PersonClass = Class.forName("com.geekby.Person");
        Constructor constructor = PersonClass.getConstructor(String.class, Integer.class);
        Person p = (Person)constructor.newInstance("Geekby", 24);
        Method m = PersonClass.getDeclaredMethod("setName", String.class);
        m.invoke(p, "newGeekby");

        System.out.println(p.getName());
    }
}
方法说明
getMethod(String name, Class… parameterTypes)获得该类某个公有的方法
getMethods()获得该类所有公有的方法
getDeclaredMethod(String name, Class… parameterTypes)获得该类某个方法
getDeclaredMethods()获得该类所有方法

3.5 通过反射访问属性

public class AccessAttribute {
    public static void main(String[] args) throws Exception {
        Class PersonClass = Class.forName("com.geekby.Person");
        Constructor constructor = PersonClass.getConstructor(String.class, Integer.class);
        Person p = (Person) constructor.newInstance("Geekby", 24);

        // name 是私有属性,需要先设置可访问
        Field f = PersonClass.getDeclaredField("name");
        f.setAccessible(true);
        f.set(p, "newGeekby");
        System.out.println(p.getName());
    }
}
方法说明
getField(String name)获得某个公有的属性对象
getFields()获得所有公有的属性对象
getDeclaredField(String name)获得某个属性对
getDeclaredFields()获得所有属性对象

3.6 利用反射执行代码

public class Exec {
    public static void main(String[] args) throws Exception {
      
      //java.lang.Runtime.getRuntime().exec("calc");
      Class runtimeClass = Class.forName("java.lang.Runtime");
      // getRuntime 是静态方法,invoke 时不需要传入对象
      Object runtime = runtimeClass.getMethod("getRuntime").invoke(null);
      runtimeClass.getMethod("exec", String.class).invoke(runtime,"open /System/Applications/Calculator.app");
    }
}

以上代码中,利用了 Java 的反射机制把我们的代码意图都利用字符串的形式进行体现,使得原本应该是字符串的属性,变成了代码执行的逻辑,而这个机制也是后续的漏洞使用的前提。

tips

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

  • 如果该方法为普通方法,那么第一个参数是类对象

  • 如果该方法为静态方法,那么第一个参数是类或 null

此外,另一种常用的执行命令的方式 ProcessBuilder ,通过反射来获取其构造函数,然后调用 start() 来执行命令:

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

查看文档可知: ProcessBuilder 有两个构造函数:

  • public ProcessBuilder(List<String> command)
  • public ProcessBuilder(String... command)

上面通过反射的调用方式使用了第一种形式的构造函数。

但是,上述的 Payload 用到了 Java 里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中) 是没有这种语法的。因此,仍需利用反射来执行 start 方法。

Class clazz = Class.forName("java.lang.ProcessBuilder");

clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("open", "/System/Applications/Calculator.app")));

上述的第二种构造函数如何调用呢?

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

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

因此,对于反射来说,如果目标函数里包含可变长参数,传入数组即可。

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

在调用 newInstance 的时候,因为该函数本身接收的是一个可变长参数:

传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个 Payload 如下:

Class clazz = Class.forName("java.lang.ProcessBuilder");

clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"open", "/System/Applications/Calculator.app"}}));

3.7 反序列化漏洞与反射

在安全研究中,使⽤反射的⼀⼤⽬的,就是绕过某些沙盒。比如,上下文中如果只有 Integer 类型的数字,如何获取到可以执行命令的 Runtime 类:

比如可以这样(伪代码): 1.getClass().forName("java.lang.Runtime")

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文