什么是Java反射?

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

反射就是把Java类中的各种成分映射成一个个的Java对象。

在Java中,每个类都对应一个Class对象,其反射机制是通过获取该类的Class对象,然后获取到其类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,同时可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等来体现的。

反射机制

获取Class对象

  1. 通过Class类中的静态方法forName()

    1
    Class.forName("全类名") // 包名.类名
  2. 通过任何数据类型都有的class属性

    1
    类名.class属性
  3. 通过Object类的getClass()方法

    1
    对象.getClass()方法

    例子:获取Person类对应的Class对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import domain.Person;

public class Demo {
public static void main(String[] args) throws ClassNotFoundException{
Class c1=Class.forName("domain.Person");
Class c2=Person.class;
Person p=new Person("zhangsan",18);
Class c3=p.getClass();
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
}
}
//class domain.Person
//class domain.Person
//class domain.Person

获取内部类的Classs对象

Java的普通类 C1 中支持编写内部类 C2 ,而在编译的时候,会生成两个文件: C1.classC1$C2.class,可以把他们看作两个无关的类。即Class.forName("C1$C2")可以获取到该内部类的Class对象。

类的初始化

默认情况下

Class.forName(className, true, currentLoader)

第二个参数为true表示在获取Class对象时,对类进行初始化。

那么初始化时,其调用的是类的什么方法呢?

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

public class Person {
{
System.out.println("调用初始块");
}
static {
System.out.println("调用了静态初始块");
}
public Person(){
System.out.println("调用了构造函数");
}
}

获取Class对象时

1
2
3
4
5
6
7
8
import domain.Person;

public class Demo {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("domain.Person");
}
}
// 调用了静态初始块

实例化时

1
2
3
4
5
6
7
8
9
10
import domain.Person;

public class Demo {
public static void main(String[] args) throws ClassNotFoundException{
Person p=new Person();
}
}
// 调用了静态初始块
// 调用初始块
//调用了构造函数

可以看到类的初始化是调用的静态初始块,实例化时会先调用静态初始快、初始块最后是构造函数。

补充:具有父类的类的实例化:父类静态初始块->子类静态初始块->父类初始块->父类构造函数->子类初始块->子类构造函数

具有父类的类的初始化:父类静态初始块->子类静态初始块

利用点:

  • 如果程序Class.forName(name) 中,name 参数可控,那么我们可以编写⼀个恶意类,将恶意代码放置在 static {} 中,从⽽执⾏恶意代码。
  • 如果程序中某个类的static代码块可控,那么我们也可以对其修改然后通过Class.forName(name)调用。

获取成员变量

  • Fields[] getFields():只能获取所有public修饰的成员变量

  • Fields getField(String name):获取特定成员变量

  • Fields[] getDeclaredFields():获取所有的成员变量,【不考虑】修饰符

  • Fields getDeclaredField(String name):获取特定的成员变量,【不考虑】修饰符

获取构造方法

  • Constructor<?>[] getConstructors(): 只能获取所有public修饰的构造方法

  • Constructor getConstructor(类 … parameterTypes): 获取特定构造方法

  • Constructor<?> getDeclaredConstructors(): 获取所有构造方法,【不考虑】修饰符

  • Constructor getDeclaredConstructor(类 … parameterTypes``): 获取特定的构造方法,【不考虑】修饰符

获取成员方法

  • Method[] getMethods(): 获取所有【public】修饰的方法,父类Object的方法也能看到

  • Method getMethod(String name,类 <?> … parameterTypes): 获取特定成员方法

  • Method[] getDeclaredMethods(): 获取所有声明方法 不考虑修饰符

  • Method getDeclaredMethod(String name,类 <?> … parameterTypes):获取特定成员方法,【不考虑】修饰符

创建类实例

1
Class对象.newInstance()

执行该方法时会调用该类的==公有无参构造方法==对该类进行实例化。

调用类方法

假设获取到的方法对象为mt

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

修改成员变量/方法权限

假设获取到的变量对象为field

1
2
3
4
field.set(类实例对象, 修改后的值);
//修改成员变量
field.setAccessible(true)
//修改成员变量访问权限

同理也可以对方法进行访问权限修改,设方法对象为mt

1
mt.setAccessible(true)

反射的运用

一般运用

如下一个类

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

public class Person {
public Person(){

}
public void eat(){
System.out.println("正在吃饭");
}
}

正常实例化并调用其方法

1
2
3
4
5
6
7
8
9
import domain.Person;

public class Demo {
public static void main(String[] args){
Person p=new Person();
p.eat();
}
}
//正在吃饭

运用反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.reflect.Method;

public class Demo {
public static void main(String args[]) {
try {
//获取Class类对象
Class clazz = Class.forName("domain.Person");
//获取指定方法
Method mt = clazz.getDeclaredMethod("eat", null);
//创建实例
Object p = clazz.newInstance();
//调用该方法
mt.invoke(p);
}catch (Exception e){
e.printStackTrace();
}
}
}
//正在吃饭

其可以简化为

1
2
Class clazz = Class.forName("domain.Person");
clazz.getDeclaredMethod("eat").invoke(clazz.newInstance());

单例模式下

上面写过,Class对象.newInstance() 会调用该类的公有无参构造方法对该类进行实例化,那么如果该类是”单例模式”呢?

单例模式,构造函数为私有,只能通过静态方法去获取该对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingleObject {

//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();

//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}

//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}

public void showMessage(){
System.out.println("Hello World!");
}
}

实例化对象

1
2
3
4
5
6
7
8
9
10
public class SingletonPatternDemo {
public static void main(String[] args) {

//获取唯一可用的对象
SingleObject object = SingleObject.getInstance();

//显示消息
object.showMessage();
}
}

此时就不能用上面哪种方法去实例化对象了,但同时也给出了另一种实例化对象的方式,调用其静态方法。

看一个真实的例子,java.lang.Runtime因为有一个exec方法可以执行本地命令,所以在很多的payload中我们都能看到反射调用Runtime类来执行本地系统命令,但恰好它也是个单例类。

那么如果我要利用它构造payload执行命令,应当进行如下构造

1
2
3
4
5
6
7
8
9
10
public class Demo {
public static void main(String args[]) {
try {
Class clazz = Class.forName("java.lang.Runtime");
clazz.getDeclaredMethod("exec", String.class).invoke(Runtime.getRuntime(),"calc.exe");
}catch (Exception e){
e.printStackTrace();
}
}
}

指定构造方法实例化

那假设它不是单例模式,而且也没有无参构造方法呢?

此时我们就要指定构造方法对其进行实例化,从而调用其方法,这里以另一个常用来执行命令的ProcessBuilder类为例。

此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。它有两个构造方法,但都是有参构造

1
2
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)

可以看下它的一般用法

1
2
3
4
//利用指定的操作系统程序和参数构造一个进程生成器
ProcessBuilder pb = new ProcessBuilder("myCommand", "myArg1", "myArg2");
// //启动进程
Process p = pb.start();

那么如何利用反射进行调用呢?

我们可以指定构造方法去进行实例化,然后调用start()方法执行传入的命令(程序)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.Arrays;
import java.util.List;

public class Demo {
public static void main(String args[]) {
try {//获取Class类对象
Class clazz = Class.forName("java.lang.ProcessBuilder");
//获取start方法,并用getDeclaredConstructor根据传参类型获取指定的构造方法,然后newInstance传入参数进行实例化,以invoke调用
clazz.getMethod("start").invoke(clazz.getDeclaredConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
}catch (Exception e){
e.printStackTrace();
}
}
}

上面我们是用的第一个构造参数,那么这次用第二个。

可以注意到的是,该构造函数的参数类型应为可变长参数,即同类型数组,在这里是字符串数组。

而且这里需要再提一下,newInstance 方法的参数为Object数组。这意味着我们向newInstance 传参时应当传入一个二维数组。

1
2
3
4
5
6
7
8
9
10
public class Demo {
public static void main(String args[]) {
try {
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getDeclaredConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
}catch (Exception e){
e.printStackTrace();
}
}
}

执行私有方法

这里考虑最后一种情况,构造方法为私有的情况,这次依然以Runtime类为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.reflect.Constructor;
public class Demo {
public static void main(String args[]) {
try {//获取Class类对象
Class clazz = Class.forName("java.lang.Runtime");
//获取指定构造方法
Constructor cs = clazz.getDeclaredConstructor();
//设置为可访问权限
cs.setAccessible(true);
//调用执行
clazz.getMethod("exec", String.class).invoke(cs.newInstance(),"calc.exe");
}catch (Exception e){
e.printStackTrace();
}
}
}

参考

Java安全之反射机制