Java反射
反射有关的类方法
首先给出一个例子
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 来获取
在安全研究中,我们使⽤反射的⼀⼤⽬的,就是绕过某些沙盒。⽐如,上下⽂中如果只有Integer类型的 数字,我们如何获取到可以执⾏命令的Runtime类呢?也许可以这样(伪代 码):1.getClass().forName("java.lang.Runtime")
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.lang.Runtime
第二个参数initialize
其实不是初始化构造函数,即使它为True。构造函数并不会执行。
这个初始化可以理解为类的初始化。
demo
import java.io.IOException;
public class reflect {
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
public static 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());
}
}
public static void main(String[] args) {
TrainPrint trainPrint=new TrainPrint();
}
}
输出的时候我们发现最先输出的是static{},然后是{},最后才是构造方法。
其中,static{}
是在“类初始化”的时候调用的。所以说,forName
中的initialize=true
其实就是告诉Java虚拟机是否执⾏”类初始化“。
那么,假如有如下函数,name可控。
public static void ref(String name) throws Exception {
Class.forName(name);
}
我们就可以编写一个恶意类,把恶意代码放在static{}中,从而执行。
import java.lang.Runtime;
import java.lang.Process;
public class Eval {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
当然,如何将这个恶意类带入机器,之后我们再研究
invoke,getMethod
在正常情况下,除了系统类,如果我们想拿到一个类,需要先 import 才能使用。而使用forName就不 需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。
另外,我们经常在一些源码里看到,类名的部分包含 $
符号,比如fastjson在 checkAutoType 时候就会 先将 $
替换成 .
:https://github.com/alibaba/fastjson/blob/fcc9c2a/src/main/java/com/alibaba/fa stjson/parser/ParserConfig.java#L1038。 $
的作用是查找内部类。
Java的普通类 C1
中支持编写内部类 C2
,而在编译的时候,会生成两个文件: C1.class 和 C1$C2.class
,我们可以把他们看作两个无关的类,通过 Class.forName("C1$C2")
即可加载这个内部类。
获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法。
class.newInstance()
的作用就是调用这个类的无参构造函数,这个比较好理解。不过,我们有时候 在写漏洞利用方法的时候,会发现使用 newInstance
总是不成功,这时候原因可能是:
- 你使用的类没有无参构造函数
- 你使用的类构造函数是私有的
最最最常见的情况就是 java.lang.Runtime ,这个类在我们构造命令执行Payload的时候很常见,但我们不能直接这样来执行命令:
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
原因就是Runtime
类的构造方法是私有的。
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
的时候,我们需要 传给他你需要获取的函数的参数类型列表。
比如这里的 Runtime.exec
方法有6个重载:
我们使用最简单的,也就是第一个,它只有一个参数,类型是String,所以我们使用 getMethod("exec", String.class)
来获取Runtime.exec
方法。
invoke 的作用是执行方法,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数是类
这也比较好理解了,我们正常执行方法是 [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
我们需要用到一个新的反射方法 getConstructor
。 和getMethod
类似, getConstructor
接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。
比如,我们常用的另一种执行命令的方式ProcessBuilder
,我们使用反射来获取其构造函数,然后调用 start()
来执行命令:
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
。
但是,我们看到,前面这个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
了。
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
修改它的作用域,否则仍然不能调用。