java反射机制

JAVA反射机制

-----copy自p牛的java安全漫谈

反射是⼤多数语⾔⾥都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将Java这种静态语⾔附加上动态特性。

PHP本身拥有很多动态特性,所以可以通过“⼀句话⽊⻢”来执⾏各种功能;Java虽不像PHP那么灵活,但其提供的“反射”功能,也是可以提供⼀些动态特性。⽐如,这样⼀段代码,在你不知道传⼊的参数值的时候,你是不知道他的作⽤是什么的:

public void execute(String className, String methodName) throws Exception {
 Class clazz = Class.forName(className);
 clazz.getMethod(methodName).invoke(clazz.newInstance());
}

上⾯的例⼦中,演示了⼏个在反射⾥极为重要的⽅法:

  • 获取类的⽅法: forName
  • 实例化类对象的⽅法: newInstance
  • 获取函数的⽅法: getMethod
  • 执⾏函数的⽅法: invoke

基本上,这⼏个⽅法包揽了Java安全⾥各种和反射有关的Payload。

forName 不是获取“类”的唯⼀途径,通常来说我们有如下三种⽅式获取⼀个“类”,也就
是 java.lang.Class 对象:

  • obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过obj.getClass() 来获取它的类
  • Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接
    拿它的 class 属性即可。这个⽅法其实不属于反射。
  • Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取

forName有两个函数重载:

  • Class<?> forName(String name)
  • Class<?> forName(String name, boolean initialize, ClassLoader loader)

第⼀个就是我们最常⻅的获取class的⽅式,其实可以理解为第⼆种⽅式的⼀个封装:

Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)

默认情况下, forName 的第⼀个参数是类名;第⼆个参数表示是否初始化(“类初始化”);第三个参数就是 ClassLoader

ClassLoader 是什么呢?它就是⼀个“加载器”,告诉Java虚拟机如何加载这个类。关于这个点,后⾯还有很多有趣的漏洞利⽤⽅法,这⾥先不展开说了。Java默认的 ClassLoader 就是根据类名来加载类,这个类名是类完整路径,如 java.lang.Runtime 。

什么是初始化?

public class TrainPrint {
 {
		 System.out.printf("Empty block initial %s\n", this.getClass());
 }
 static {
		 System.out.printf("Static initial %s\n", TrainPrint.class);
 }
 public TrainPrint() {
			 System.out.printf("Initial %s\n", this.getClass());
 }
}

上述的代码执行后⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数。

其中, static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯,
但在当前构造函数内容的前⾯。所以说, forName 中的 initialize=true 其实就是告诉Java虚拟机是否执⾏”类初始化“

$的作用就是查找内部类。Java的普通类 C1 中支持编写内部类 C2 ,而在编译的时候,会生成两个文件: C1.class 和C1$C2.class ,我们可以把他们看作两个无关的类,通过 Class.forName("C1$C2") 即可加载这个内部类。

获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方
法。

class.newInstance() 的作用就是调用这个类的无参构造函数,这个比较好理解。不过,我们有时候
在写漏洞利用方法的时候,会发现使用 newInstance 总是不成功,这时候原因可能是:

  1. 你使用的类没有无参构造函数
  2. 你使用的类构造函数是私有的

最最最常见的情况就是 java.lang.Runtime ,这个类在我们构造命令执行Payload的时候很常见,但
我们不能直接这样来执行命令:

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

然后就会得到如下异常

Untitled

原因是 Runtime 类的构造方法是私有的。

Untitled

有同学就比较好奇,为什么会有类的构造方法是私有的,难道他不想让用户使用这个类吗?这其实涉及到很常见的设计模式:“单例模式”。(有时候工厂模式也会写成类似)
比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取:

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

这样,只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance 获取这个对象,避免建
立多个数据库连接。
回到正题,Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对
象。我们将上述Payload进行修改即可正常执行命令了:

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

这里用到了 getMethod 和 invoke 方法。

getMethod 的作用是通过反射获取一个类的某个特定的公有方法。而学过Java的同学应该清楚,Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,我们需要传给他你需要获取的函数的参数类型列表。

Untitled

比如exec里面就有6个重载

Untitled

这里我们使用第一个,最简单的进行命令执行

Untitled

它只有一个参数,类型是String,所以我们使用getMethod("exec", String.class) 来获取 Runtime.exec 方法。

invoke 的作用是执行方法,它的第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类

Untitled

这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是
method.invoke([1], [2], [3], [4]...) 。

所以我们的Payload分解一下就是:

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。

Untitled

和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,
所以必须用参数列表类型才能唯一确定一个构造函数。获取到构造函数后,我们使用 newInstance 来执行。

Untitled

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

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

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String... command)

Untitled

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

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了。

Untitled

那么,如果我们要使用 public ProcessBuilder(String... command) 这个构造函数,需要怎样用反
射执行呢?
这又涉及到Java里的可变长参数(varargs)了。正如其他语言一样,Java也支持可变长参数,就是当你定义函数的时候不确定参数数量的时候,可以使用 ... 这样的语法来表示“这个函数的参数个数是可变的”。
对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的(也就不能重载):

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

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

String[] names = {"hello", "world"};
hello(names);

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

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

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

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

所以改成完整反射的payload就是:

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

再说到第二个问题,如果一个方法或构造方法是私有方法,我们是否能执行它呢?

这就涉及到 getDeclared 系列的反射了,与普通的 getMethod 、 getConstructor 区别是:

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

getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor 的具体用法和
getConstructor 类似

举个例子,前文我们说过Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime() 来
获取对象。其实现在我们也可以直接用 getDeclaredConstructor 来获取这个私有的构造方法来实例
化对象,进而执行命令:

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

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

Untitled

posted @ 2023-03-01 11:12  z2n3  阅读(59)  评论(0编辑  收藏  举报