Java反射笔记3

在正常情况下,除了Java自带的类,我们要想使用一个类,就需要我们导入即import才能使用,所以forName就显得特别重要,因为我们可以拿这个来加载任意类。

在一些情况中,类名的部分会有美元$这个符号,$的作用是查找内部类。

在Java中,普通类c1中支持编写内部类c2,在编译时,会生成两个文件:c1.class和c1$c2.class,可以把他们看做两个不相关的类,通过class.forName就可以加载这个内部类

获取了这个类,就能接着通过反射来获取属性,方法,以及进行实例化。

newInstance()是调用类的无参构造函数,不过有时使用这个方法时会不成功,主要有两点

  1. 想调用的类没有无参构造函数
  2. 想调用的类构造函数是私有的
    举个例子,在构造命令执行相关Payload时,一般会使用Runtime这个类,但不能直接这样执行命令
1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id"); 

会有报错!
原因是Runtime类的构造方法是私有的,而我们是不能直接使用的,所以会有报错,这里其实就使用了单例设计模式。

比如说,在Web网站中会使用数据库进行大量数据交互,但数据库链接只需要一次链接就行,不是每次使用时还要重新建立一个新的链接,此时将建立数据库链接的相关构造类的构造函数设置为私有,编写一个静态方法,使用单例模式

1
2
3
4
5
6
7
8
9
10
public class TrainDB { 
private static TrainDB instance = new TrainDB();

public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}

代码如图所示,只有类初始化时才执行一次构造函数,否则只能通过getInstance来获取这个对象,避免建立多个数据库链接
Runtime类是很明显的一个单例模式设计例子,只能通过Runtime.getRuntime()来获取Runtime对象,所以要将上面的payload进行修改

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

getMethod和invoke都是反射中很熟悉的方法,又因为在Java语法中类是可以进行重载的,不能仅仅通过函数名来确定一个函数。所以,调用getMethod的时候,需要传给需要获取的函数的参数类型列表,这里使用的Runtime.exec这个方法就有6个重载

在这里我们使用最简单的,只有一个参数类型是String,所以使用getMethod(“exec”,String.class)来获取Runtime.exec方法

invoke()作用是执行方法:

  1. 如果这个方法是一个普通方法,第一个参数就是类对象
  2. 如果这个方法是一个静态方法,第一个参数就是类
    所以将上述代码进行简化
1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime"); 
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

这下会清楚很多
那如果一个类没有无参构造方法,也没有单例模式里的静态方法,如何通过反射实例化该类?

为了解决这个问题,我们需要引入一个新的反射方法getConstructor

与getMethod类似,getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载,有无参数即是一个体现。

获取想要的构造函数,那我们就要用newInstance来执行

比如另一种执行命令的方式ProcessBuilder,用反射来获取其构造函数,调用start()来执行命令

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

ProcessBuilder拥有两个构造函数

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String… command)
    这里用到了第一个形式的构造函数,所以在getConstructor传入的是List.class,但这里很明显使用了一个强制类型转换,所以需要一个反射来完成
1
2
Class clazz = Class.forName("java.lang.ProcessBuilder"); 
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance( Arrays.asList("calc.exe")));

通过 getMethod(“start”) 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是 ProcessBuilder Object了。
那如果采取使用第二个构造函数呢?这里面涉及了可变长参数,在Java里如果定义函数时不确定参数数量的时候,可以使用…这样的语法,来表示这个函数的参数个数是可变的。对于可变长参数,java其实在编译的时候会编译成一个数组

1
2
public void hello(String[] names) {} 
public void hello(String...names) {} #不能重载

所以我们如果有一个数组,就可以直接传给我们想使用的函数,对于反射来说同理,在这里把字符串数组的类String[].class传给getConstructor,获取第二种构造函数

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

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

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

如果一个方法或构造方法是私有的呢?

这里就要使用getDeclared系列的反射,与普通的区别在于

  • getMethod系列方法获取的是当前类中所有公共方法,包括继承的方法
  • getDeclaredMethod系列方法获取的是当前类中的声明过的方法,是确确实实写在类里的,包括私有的,但是继承的类不包括
    具体使用方法是与getMethod等是类似的

在前面说过Runtime这个类的构造函数是私有的,就要用Runtime.getRuntime()来获取对象,现在也可以用getDeclareConstructor来获取这个私有的构造方法进行实例化

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclareConstructor();
m.setAccessible(true);
clazz.getMethod("exec",String.class).invoke(m.newInstance(),"calc.exe");

这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。