JavaSec-java的反射详解


java的反射机制基础

Java反射(Reflection)是Java非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。Java反射机制是Java语言的动态性的重要体现,也是Java的各种框架底层实现的灵魂。

一般情况下我们使用某个类的时候,比如创建个对象或者直接引用等,都会必定知道他是什么类,是用来做什么的。于是我们可以直接对其进行实例化,之后使用这个类实例化出来的对象进行操作

Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);

上面这个样子的初始化我们可以理解为:”正”,而发射则是我们一开始并不知道我们要初始化的类对象是什么,自然也无法通过上述例子进行实例化操作。以及调用。
这时候,我们使用 JDK 提供的反射 API 进行反射调用:

Class clz = Class.forName("reflect.Apple");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);

上面两段代码的执行结果,其实是完全一样的。但是其思路完全不一样,第一段代码在未运行时就已经确定了要运行的类(Apple),而第二段代码则是在运行时通过字符串值才得知要运行的类

一个简单的例子

上面提到的示例程序,其完整的程序代码如下:

public class Apple {
    private int price;
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
    public static void main(String[] args) throws Exception{
        //正常的调用
        Apple apple = new Apple();
        apple.setPrice(5);
        System.out.println("Apple Price:" + apple.getPrice());
        //使用反射调用
        Class clz = Class.forName("com.chenshuyi.api.Apple");
        Method setPriceMethod = clz.getMethod("setPrice", int.class);
        Constructor appleConstructor = clz.getConstructor();
        Object appleObj = appleConstructor.newInstance();
        setPriceMethod.invoke(appleObj, 14);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
    }
}

从代码中可以看到我们使用反射调用了 setPrice 方法,并传递了 14 的值。之后使用反射调用了 getPrice 方法,输出其价格。上面的代码整个的输出结果是:
ApplePrice:5

ApplePrice:14

从这个简单的例子可以看出,一般情况下我们使用反射获取一个对象的步骤:

  • 获取类的 Class 对象实例
    Class clz = Class.forName("com.zhenai.api.Apple");
    
  • 根据 Class 对象实例获取 Constructor 对象
    Constructor appleConstructor = clz.getConstructor();
    
  • 使用 Constructor 对象的 newInstance 方法获取反射类对象
    Object appleObj = appleConstructor.newInstance();
    
    而如果要调用某一个方法,则需要经过下面的步骤:
  • 获取方法的 Method 对象
    Method setPriceMethod = clz.getMethod("setPrice", int.class);
    
  • 利用 invoke 方法调用方法
    setPriceMethod.invoke(appleObj, 14);
    
    到这里,我们已经能够掌握反射的基本使用。但如果要进一步掌握反射,还需要对反射的常用 API 有更深入的理解。

java反射常用api

获取反射中的class对象

在反射中要获取一个类或调用一个类的方法,我们首先要获取到该类的class对象

在java API中,获取Class类对象有三种方法:

第一种,使用Class.forName 静态方法。当你知道该类的全路径名时,你可以使用该方法获取Class类对象

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

第二种,使用类的.class方法,这种方法只适合在编译前就知道操作的Class

Class clz = String.class;

第三种,使用类对象的getClass()方法。

String str = new String("hello");
Class clz = str.getClass();

通过以上任意一种方式就可以获取Class对象了,反射调用内部类的时候需要使用$来代替.,如Test类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:Test$Hello

通过反射创建类对象

通过反射创建类对象主要有两种方式:通过Class对象的newInstance()方法,通过Constructor对象的newInstance()方法

第一种,通过Class对象的newInstance()方法

Class clz =Apple.class;
Apple apple = (Apple)clz.newInstance();

第二种,通过Constructor 对象的newInstance()方法

Class clz = Apple.class;
Constructor sonstructor = clz.getConstructor();
Apple apple =(Apple)constructor.newInstance();

使用Constructor对象创建类对象可以选择特定构造方法,而通过Class对象则只能使用默认的无参数构造方法。下面的代码就调用了一个有参数的构造方法进行了类对象的初始化。

Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String,class,int.class);
Apple apple = (Apple)constructor.newInstance("红富士"15)

通过反射获取类属性,方法,构造器

我们通过Class对象的getFields()方法可以获取Class类的属性。但无法获取私有属性。

Class clz = Apple.class;
Field [] fields = clz.getFields();
for (Field field : fields){
    System.out.println(field.getName());
}

输出结果是price
而如果使用Class对象的getDeclareFields()方法则可以获取包括私有属性在内的所有属性:

Class clz = Apple.class;
Field [] fields = clz.getDeclareFields()
for (Field field : fields){
   System.out.println(field.getName());
}

输出结果是
name

price

与获取类属性一样,当我们去获取类方法、类构造器时,如果要获取私有方法或私有构造器,则必须使用有 declared 关键字的方法。

反射java_lang_Runtime进行rce及一点补充

java.lang.Runtime因为有一个exec方法可以执行命令,所以在很多的payload中我们都可以看到反射调用Runtime类来执行本地系统命令,通过学习如何反射Runtime类也能让我们理解反射的一些基础用法以及一些攻击手法

不使用反射执行本地命令代码片段:

System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(),"UTF-8"));

如上可以看到,我们可以使用一行代码完成本地的rce,但是入砂锅使用反射就会比较麻烦了,我们不得不需要间接性的调用Runtime的exec方法
反射Runtime执行本地命令执行代码:

//获取Runtime类对象
Class runtimeClass1 = Class.forname("java.lang.Runtime");
//获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
//如果方法是 private修饰的,当你用反射去访问的时候
//setAccessible(true); 之后 才能访问
setAccessible(true);
//创建Runtime类实例,等价于Runtime rt=new Runtime();
object runtimeInstance=constructor.newInstance();
//获取Runtime的exec(string cmd)方法
//getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
//getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了
Method runtimeMethod = runtimeClass1.getMethod("exec",String.class);
//调用exec方法,等价于exec(String cmd)方法
//method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);
Process runtimeMethod = (Process) runtimeMethod.invoke(runtimeInstance,cmd);
//获取命令执行结果
InputStream in = process.getInputStream();
//输出命令执行结果
System.out.println(IOUtiles.tostring(in,"UTF-8"));

反射调用Runtime实现本地命令执行的流程如下:

1.反射获取Runtime类对象(Class.forName(“java.lang,Runtime”))

2.使用Runtime类的Class对象获取Runtime类的无参构造方法(getDeclareConstructor()),因为Runtime的构造方法是private的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限(constructor.setAccessible(true))。

3.获取Runtime类的exec(string)方法(runtimeClass1.getMethod(“exec”,String.class);)。

4.调用exec(String)方法(runtimeMethod.invoke(runtimeInstance,cmd))。

上面的代码每一步都写了非常清晰的注释,接下来我们将进一步深入的了解下每一步具体含义。

反射创建类实例

在java的任何一个类都必须有一个或者多个构造方法,如果代码中没有创建构造方法那么在类编译的时候会自动创建一个无参数的构造方法

Runtime类构造方法示例代码片段:

public class Runtime {
   /** Don't let anyone else instantiate this class */
  private Runtime() {}
}

从上面的Runtime类代码注释我们可以看到他的构造方法是私有方法,看出来其本身是不希往望除了其自身以外的任何人去创建该类实例的,所以我们没办法new一个Runtime类实例即不能实用Runtime aa = new Runtime();的方式去创建Runtime对象,在示例中我们借助了反射机制,修改了方法访问权限从而间接的创建出了Runtime对象。
runtimeClass1.getDeclaredConstructor和runtimeClass1.getConstructor都可以获取到类的构造方法,区别在于后者无法获取到私有方法,所以一般在获取某个类的构造方法时侯,我们会去使用前者去获取构造方法。如果构造方法中有一个或多个参数的情况下我们应该在获取构造方法时候传入对应得参数类型数组,如:

clazz.getDeclaredConstructor(String.class,String.class)。

如果我们想获取类的所有构造方法我们可以使用:

clazz.getDeclaredConstructors来获取一个Constructor数组

获取到Constructor以后我们可以通过constructor.newInstance()来创建类实例,同理如果有参数的情况下我们应该传入对应的参数值。

如:constructor.new.Instance(“admin”,”123456”).当我们没有权限访问构造方法时,我们应该调用constructor.setAccessible(true)来修改访问权限就可以成功得创建出类的实例了。

反射调用类方法

Class对象提供了一个获取某个类的所有的成员方法的方法,也可以通过方法名和方法参数类型来获取指定成员方法:

获取当前类所有的成员方法

Method[] methods = clazz.getDeclaredMethods()

获取当前指定类方法

Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名",参数类型如string.class,多个参数用","隔开;)

getMethodgetDeclaredMethod都能够获取到类成员方法,区别在于getMethod只能获取到当前类和父类的所有有权限的方法(如:public),而getDeclaredMethod能获取到当前类的所有成员方法(不包含父类)。
反射调用方法

获取到java.lang.reflect.Method对象以后我们可以通过Methodinvoke方法来调用类方法。

调用类方法代码片段

method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);

method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)的方式调用。
method.invoke的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型
获取反射中的class对象

反射调用成员变量

Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。

获取当前类的所有成员变量:

Field fields = clazz.getDeclaredFields();

获取当前类指定的成员变量:

Field field  = clazz.getDeclaredField("变量名");

getFieldgetDeclaredField的区别同getMethodgetDeclaredMethod
获取成员变量值:

Object obj = field.get(类实例对象);

修改成员变量值:

field.set(类实例对象, 修改后的值);

同理,当我们没有修改的成员变量权限时可以使用:field.setAccessible(true)的方式修改为访问成员变量访问权限。
如果我们需要修改被final关键字修饰的成员变量,那么我们需要先修改方法

copy
// 反射获取Field类的modifiers
Field modifiers = field.getClass().getDeclaredField("modifiers");
// 设置modifiers修改权限
modifiers.setAccessible(true);
// 修改成员变量的Field对象的modifiers值
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
// 修改成员变量值
field.set(类实例对象, 修改后的值);

反射总结

Java反射机制是Java动态性中最为重要的体现,利用反射机制我们可以轻松的实现Java类的动态调用。Java的大部分框架都是采用了反射机制来实现的(如:Spring MVCORM框架等),Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。


文章作者: wa1ki0g
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wa1ki0g !
  目录