Java反射

反射机制

反射概念

在Java中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。
让java可以在运行时,根据传入的类名字符串,去执行这个类存在的方法等。

以下从开发的角度举例了一个反射机制在开发中的作用。简单了解即可。

一个不使用反射机制的例子:

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
interface fruit{  
public abstract void eat();
}

class Apple implements fruit{
public void eat(){
System.out.println("Apple");
}
}

class Orange implements fruit{
public void eat(){
System.out.println("Orange");
}
}

// 构造工厂类
// 也就是说以后如果我们在添加其他的实例的时候只需要修改工厂类就行了
class Factory{
public static fruit getInstance(String fruitName){
fruit f=null;
if("Apple".equals(fruitName)){
f=new Apple();
}
if("Orange".equals(fruitName)){
f=new Orange();
}
return f;
}
}
class hello{
public static void main(String[] a){
fruit f=Factory.getInstance("Orange");
f.eat();
}

}

如果我们想要添加新的水果,就需要

  • 添加新的水果类
  • 修改Factory
  • 在main函数中使用新的水果类

修改为反射机制的代码:

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
interface fruit{  
public abstract void eat();
}

class Apple implements fruit{
public void eat(){
System.out.println("Apple");
}
}

class Orange implements fruit{
public void eat(){
System.out.println("Orange");
}
}

class Factory{
public static fruit getInstance(String ClassName){
fruit f=null;
try{
f=(fruit)Class.forName(ClassName).newInstance();
}catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
class hello{
public static void main(String[] a){
fruit f=Factory.getInstance("Reflect.Apple");
if(f!=null){
f.eat();
}
}
}

这时候如果我们需要添加水果,只需要

  • 添加水果类
  • 在main函数中使用新的水果类即可

那么好像可以看出一点问题,如果传入的类名可控,再加上一些办法,那就不是可以调用任意类,去运行系统命令了呢。或者执行危险的命令

反射组成相关的类

反射机制相关操作一般位于java.lang.reflect包中。

image-20221104151906595

image-20221104151855365

而java反射机制组成需要重点注意以下的类:

java.lang.Class:类对象;

java.lang.reflect.Constructor:类的构造器对象;

java.lang.reflect.Field:类的属性对象;

java.lang.reflect.Method:类的方法对象;

反射常见使用的方法

获取类的方法:forname

实例化类对象的方法:newInstance

获取函数的方法:getMethod

执行函数的方法:invoke

class对象的获取方法

java反射机制的原理基础是理解Class类,在反射中,我们想获取一个类或调用一个类的方法,需要先获取到该类的Class对象。

对于普通用户我们可以采用以下方法创建实例:

1
Person test = new Person();

而我们在创建class类的实例对象却不能使用上述方法,运行会抛出错误

1
Class test = new Class();

image-20221104152231575

同时我们可以跟进Class类的源码进行查看,发现其构造器是私有的,所以只有JVM能够创建Class对象。

image-20221104152244354

因为Class类是private私有属性,我们也无法通过创建对象的方式来获取class对象,那么我们怎样才能够获取到class对象呢?一般我们获取class对象就有以下三种方法,我们来逐一看看。

1、类的.class属性

第一种就是最简单明了的方式,我们可以通过类名的属性class获取。

1
Class c1=ReflectDemo.class;

ReflectDemo是一个已经加载的类,想要获取它的java.lang.Class对象,直接拿取class参数即可。(这不是反射机制)

2、实例化对象的getClass()方法

第二种我们可以先实例化一个对象,之后在调用getClass()方法。

1
2
ReflectDemo demo2= new ReflectDemo();
Class c2 = demo2.getClass();
  1. 假如obj是实例:获取该实例的class(如Runtime.getRuntime().getClass()结果就是class java.lang.Runtime类)(此处类的意思实际上时class这个类的对象)
  2. 假如obj是类:获取到java.lang.Class类(class这个类的对象)

image-20221104153931331

3、Class.forName(String className):动态加载类

第三种则是调用Class类中的forName方法,将字节码文件加载进内存,返回Class对象。

如果知道类的名字,可以直接使用forname来获取。

forName两种使用形式

1
2
3
4
5
6
7
8
9
10
Class<?> forName(String name) 
//name:class名称
Class<?> forName(String name, **boolean** initialize, ClassLoader loader)
//name:class名称
//initialize:是否进行“类初始化”
//loader:加载器

//第一种调用形式等同于第二种,其实就是第二种形式的封装,默认进行"类初始化”,默认加载器根据类名(完整路径)来加载
Class.forName(className)
Class.forName(className, true, currentLoader)

类初始化
类初始化不等于类的实例化,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo1 {
//初始化块
{
System.out.println("初始化块:"+this.getClass());
}
//静态初始化块
static {
System.out.println("静态初始块:"+Demo1.class);
}
//构造函数
public Demo1(){
System.out.println("构造函数:"+this.getClass());
}
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.test.Demo1");
}
}

image-20221104154841720

image-20221104155005963

类的实例化:静态初始块->初始块->构造函数
类的初始化:静态初始块

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

写个简单的示例代码,分别利用这三种方法获取当前类Class对象的当前类名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> c1 = Demo2.class;
System.out.println(c1);

Demo2 demo2 = new Demo2();
Class<?> c2 = demo2.getClass();
System.out.println(c2);

Class<?> c3 = Class.forName("com.test.Demo2");
System.out.println(c3);

}
}

image-20221104155645490

在这三种获取CLass类方式中,我们一般使用第三种通过Class.forName方法去动态加载类。且使用forName就不需要import导入其他类,可以加载我们任意的类。

而使用类.class属性,需要导入类的包,依赖性太强,在大型项目中容易抛出编译错误;

而使用实例化对象的getClass()方法,需要本身创建一个对象,本身就没有了使用反射机制意义。

所以我们在获取class对象中,一般使用Class.forName方法去获取。

获取成员变量Field

获取成员变量Field位于java.lang.reflect.Field包中

Field[] getFields() :获取所有public修饰的成员变量

Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符

Field getField(String name) 获取指定名称的 public修饰的成员变量

Field getDeclaredField(String name) 获取指定的成员变量

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
import java.lang.reflect.Field;

public class FieldTest {
public String name;
public String profession;
protected int age;
private String number;
char sex;

public static void main(String[] args){
try{

Class c1 = Class.forName("com.test.FieldTest"); // 创建Class对象

Field[] fieldArray1 = c1.getDeclaredFields(); //获取全部成员变量
Field[] fieldArray2 = c1.getFields();// 获取全部public成员变量

for (Field field : fieldArray1){
System.out.println(field.getName());
}

System.out.println("-------分割线---------");

for (Field field : fieldArray2){
System.out.println(field.getName());
}
System.out.println("-------分割线---------");

Field fieldArray3 = c1.getField("name"); // 获取指定名称的public修饰的成员变量
System.out.println(fieldArray3.getName());
System.out.println("-------分割线---------");

Field fieldArray4 = c1.getDeclaredField("number"); // 获取指定的成员变量
System.out.println(fieldArray4.getName());

} catch (Exception e) {
e.printStackTrace();
}
}
}

image-20221104161641729

获取成员方法Method

Method getMethod(String name, 类<?>… parameterTypes) //返回该类所声明的public方法

Method getDeclaredMethod(String name, 类<?>… parameterTypes) //返回该类所声明的所有方法

//第一个参数获取该方法的名字,第二个参数获取标识该方法的参数类型

Method[] getMethods() //获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法

Method[] getDeclaredMethods() // 获取该类中的所有方法

我们使用getMethod作用通过反射获取一个类的某个特定的公有方法。且java中支持类的重载,我们不能仅通过函数名确定一个函数。在调用getMethod时候,需要传给他你需要获取的函数的参数类型列表。

如Runtime.exec方法有6个重载:

image-20221104161937066

Class.forName(“java.lang.Runtime”).getMethod(“exec”, String.class)

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
package com.test;

import java.lang.reflect.Method;

public class MethodTest {
public void study(String s) {
System.out.println("学习中..." + s);
}

protected void run() {
System.out.println("跑步中...");
}

void eat() {
System.out.println("吃饭中...");
}

private String sleep(int age) {
System.out.println("睡眠中..." + age);
return "sleep";
}

public static void main(String[] args) {
try {
Class c = Class.forName("com.test.MethodTest"); // 创建Class对象
Method[] methods1 = c.getDeclaredMethods(); // 获取所有该类中的所有方法
Method[] methods2 = c.getMethods(); // 获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法

for (Method m:methods1) {
System.out.println(m);
}

System.out.println("-------分割线---------");

for (Method m:methods2) {
System.out.println(m);
}

System.out.println("-------分割线---------");

Method methods3 = c.getMethod("study", String.class); // 获取study方法
System.out.println(methods3);
System.out.println("-------分割线---------");

Method method4 = c.getDeclaredMethod("sleep", int.class); // 获取sleep方法
System.out.println(method4);

} catch (Exception e) {
e.printStackTrace();
}
}
}

image-20221104162221544

获取构造函数Constructor

Constructor<?>[] getConstructors() :只返回public构造函数

Constructor<?>[] getDeclaredConstructors() :返回所有构造函数

Constructor<> getConstructor(类<?>… parameterTypes) : 匹配和参数配型相符的public构造函数

Constructor<> getDeclaredConstructor(类<?>… parameterTypes) : 匹配和参数配型相符的构造函数

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
package com.test;

import java.lang.reflect.Constructor;

public class ConstructorTest {
public ConstructorTest() {
System.out.println("无参构造函数");
}
public ConstructorTest(String name) {
System.out.println("有参构造函数" + name);
}
private ConstructorTest(boolean n) {
System.out.println("私有构造函数");
}
public static void main(String[] args) {
try {
Class c1 = Class.forName("com.test.ConstructorTest");
Constructor[] constructors1 = c1.getDeclaredConstructors();
Constructor[] constructors2 = c1.getConstructors();
for (Constructor c : constructors1) {
System.out.println(c);
}
System.out.println("-------分割线---------");
for (Constructor c : constructors2) {
System.out.println(c);
}
System.out.println("-------分割线---------");
Constructor constructors3 = c1.getConstructor(String.class);
System.out.println(constructors3);
System.out.println("-------分割线---------");
Constructor constructors4 = c1.getDeclaredConstructor(boolean.class);
System.out.println(constructors4);
} catch (Exception e) {
e.printStackTrace();
}
}
}

image-20221104162748674

invoke

invoke方法位于Method类下,其的作用是传入参数,执行方法,
public Object invoke(Object obj, Object... args)
它的第一个参数是执行method的对象:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类(之后会提到,这里其实不用那么死板,这个)
    它接下来的参数才是需要传入的参数。

由于我们的exec函数是一个普通方法,需要传入类对象,即invoke(类对象,exec方法传入的参数)
之前说到Runtime的类对象不能通过newInstance()来获取对象(class.newInstance等于new class),是因为Runtime的类构造函数是一个private构造函数,只能通过getRuntime方法返回一个对象。
获取类对象:Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")) (由于getRuntime是一个静态方法,invoke传入Runtime类,进行调用)
invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),"calc.exe")

合成以上的操作:

1
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),"calc.exe")

简化一下:

1
2
Class clazz = Class.forName("java.lang.Runtime"); 
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");

image-20221104170053363

image-20221104170108698

这种方式虽然弹出了计算器,但仅仅只是调用其方法去实现。而我们还是想执行其私有方法,通过newInstance来构造使用。这样就需要我们通过setAccessible(true)来突破访问权限的检查。

设置setAccessible(true)暴力访问权限

在一般情况下,我们使用反射机制不能对类的私有private字段进行操作,绕过私有权限的访问。但一些特殊场景存在例外的时候,比如我们进行序列化操作的时候,需要去访问这些受限的私有字段,这时我们可以通过调用AccessibleObject上的setAccessible()方法来允许访问。

还需要用到getDeclaredConstructor

这个前面介绍了 可以获取到的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了

image-20221104171205261

可以看到是私有的构造函数

不能直接newInstance来实例化类

代码如下:一样可以弹计算器

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
package com.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class RuntimeTest {
public static void main(String[] args) {
try {
Class<?> aClass = Class.forName("java.lang.Runtime");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
aClass.getMethod("exec",String.class).invoke(declaredConstructor.newInstance(),"calc.exe");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
}

image-20221104171726944

ProcessBuilder

假如一个类没有无参构造方法(即不能class.newInstance()),也没有单例模式(只存在一个实例)的静态方法(即不能像getRuntime一样获取实例),那我们该如何实例化这个类呢?

这个时候可以用ProcessBuilder 通过反射来获取其构造函数,然后调用start()来执行命令

1
2
3
4
List<String> paramList = new ArrayList<>();
paramList.add("calc.exe");
ProcessBuilder pb = new ProcessBuilder(paramList);
pb.start();

image-20221105154211799

其构造函数是写入了一个字符串,不是无参构造方法,接下来我们会一步步进行转化。

1
getConsturctor()`函数可以选定指定接口格式的构造函数(由于构造函数也可以根据参数来进行重载),即:`getConsturctor(参数类型)

选定后我们可以通过newInstance(),并传入构造函数的参数执行构造函数,即newInstance(传入的构造函数参数)

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List<String> command)
  • public ProcessBuilder(String... command)(此处,String...这种语法表示String参数数量是可变的,与String[]一样)
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
package com.test;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ProcessBuilderTest1 {
public static void main(String[] args) throws IOException {
try {
Class<?> aClass = Class.forName("java.lang.ProcessBuilder");
Constructor<?> constructor = aClass.getConstructor(List.class);
((ProcessBuilder) constructor.newInstance(Arrays.asList("calc.exe"))).start();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}


}
}

我上面用到了第一个形式的构造函数,所以我在 getConstructor 的时候传入的是 List.class 。

但是,我们看到,前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表 达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。

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
package com.test;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ProcessBuilderTest1 {
public static void main(String[] args) throws IOException {
try {
Class<?> aClass = Class.forName("java.lang.ProcessBuilder");
Constructor<?> constructor = aClass.getConstructor(List.class);
aClass.getMethod("start").invoke(constructor.newInstance(Arrays.asList("calc.exe")));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}


}
}

那么,如果我们要使用 public ProcessBuilder(String… command) 这个构造函数,需要怎样用反 射执行呢?

这又涉及到Java里的可变长参数(varargs)了。正如其他语言一样,Java也支持可变长参数,就是当你 定义函数的时候不确定参数数量的时候,可以使用 … 这样的语法来表示“这个函数的参数个数是可变 的”。

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

1
2
3
public void hello(String[] names) {} 

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

如果我们有一个数组,想传给hello函数,只需直接传即可:

1
2
3
String[] names = {"hello", "world"}; 

hello(names);

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

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

clazz.getConstructor(String[].class)

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:

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

这种形式也是类型转换

改为反射

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

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!