java反射

0x00 java反射简介

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

将类的各个部分封装为其他对象
反射是框架设计的灵魂
Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值

Java 反射主要提供以下功能:

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 在运行时调用任意一个对象的方法

反射的好处:

  1. 可以在程序运行过程中,操作这些对象
  2. 可以解耦,提高程序的可扩展性

0x01 获取class对象

java反射操作的是java.lang.Class对象,有一下方法获取一个类的Class对象:

  1. 类名.class
  2. Class.forName("com.demo.classloader.TestClass")
  3. ClassLoader.loadClass("com.demo.classLoader.TestClass")

反射调用内部类的时候需要使用$来代替.,如com.org.test类有一个叫做Hello的内部类,则在调用它的时候要写成:com.org.test$Hello

0x02 反射java.lang.Runtime

java.lang.Runtime中有一个exec方法可以执行本地命令,在很多payload中都能看见反射Runtime类来执行本地命令

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

1
2
// 输出命令执行结果
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(), "UTF-8"));

反射Runtime执行本地命令代码片段:

这里的IOUtilsorg.apache.commons.io.IOUtils包下的,需要使用maven导入

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class RunTimeTest {
public static void main(String[] args) throws IOException {

test();
}
public static void test() throws IOException {
try {
// 获取Runtime类对象
Class runtimeClass = Class.forName("java.lang.Runtime");

// 获取构造方法
Constructor constructor = runtimeClass.getDeclaredConstructor();
constructor.setAccessible(true);

// 创建Runtime类示例
Object runtimeInstance = constructor.newInstance();

// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass.getMethod("exec",String.class);

// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance,"whoami");

// 获取命令执行结果
InputStream in = process.getInputStream();

// 输出命令执行结果
System.out.println(IOUtils.toString(in,"UTF-8"));

} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e){
e.printStackTrace();
}
}

public static void Method2(){
try {
// 获取对象
Class cls = Class.forName("java.lang.Runtime");
// 获取构造方法
Constructor constructor = cls.getDeclaredConstructor();

constructor.setAccessible(true);
// 实例化对象
Object ob = constructor.newInstance();
Method mt = cls.getMethod("exec", String.class);

mt.invoke(ob,"calc");


} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e){
e.printStackTrace();
}
}
}

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

  1. 反射获取Runtime类对象(Class.forName("java.lang.Runtime"))。
  2. 使用Runtime类的Class对象获取Runtime类的无参数构造方法(getDeclaredConstructor()),因为Runtime的构造方法是private的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限(constructor.setAccessible(true))。
  3. 获取Runtime类的exec(String)方法(runtimeClass1.getMethod("exec", String.class);)。
  4. 调用exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))。

0x03 反射调用类方法

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

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

1
Method[] methods = clazz.getDeclaredMethods();

获取当前类的指定的成员方法:

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

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

反射调用方法

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

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

0x04 反射获取Runtime类执行命令

https://xz.aliyun.com/t/4711

部分代码:

1
2
3
4
5
6
7
8
9
10
Integer i = 1;
try {
Object obj = i.getClass().forName("java.lang.Runtime").
getMethod("getRuntime",new Class[]{}).invoke(null);
System.out.println(obj.getClass().getName());
i.getClass().forName("java.lang.Runtime").getMethod("exec", String.class).
invoke(obj,"calc");
} catch (Exception e) {
e.printStackTrace();
}

getMethod(方法名, 方法类型)
invoke(某个对象实例, 传入参数)
invoke的作用是执行方法,如果这个参数是一个普通方法,那么第一个参数就是类对象;如果这个方法是一个静态方法,那么第一个参数是类

0x05 反射小问题

5.1 当一个类没有无参构造放法,也没有类似单例模式里的静态方法时,如何通过反射实例化该类?

在面对以上问题时,需要用到一个新的反射方法getConstructor
这个方法和getMethod相似,getConstructor接受的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须使用参数列表类型才能唯一确定一个构造函数。

在获取到构造函数后,使用newInstance来执行

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String… command)
1
2
    Class clazz = Class.forName("java.lang.ProcessBuilder");    
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(            Arrays.asList("calc.exe")    ));

上面是使用的第一种,所以在传入的是LIst.class,通过 getMethod("start") 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是 ProcessBuilder Object了。

对于第二种构造函数,要怎么使用反射来执行呢?

这里就又涉及到java里的可变长参数(varargs)了;当定义函数的时候不确定参数数量时,就可以使用...这样的语法来表示这个函数的参数个数是可变的 ;对于可变参长数,java在编译的时候会编译成一个数组。

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

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

在调用newInstance的时候,因为这个函数本身接受的是一个可变长参数,我们在传给ProcessBuilder的也是一个可变长参数,二者叠加为一个二维数组

5.2 如果一个方法或构造方法是私有方法,我们是否能执行他

答案是可以,这里可以使用getDeclared系列的反射,

  • getDeclaredMethod系列的方法获取的是当前类中声明的方法, 是实在写在这个类里的,包括私有方法, 但从父类继承来得就不包含了
  • getMethod系列方法获取的是当前类中所有公共方法, 包括从父类中继承的方法

0x06 Java反射机制总结

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

0x07 参考文章