说在前边:该文章是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来获取
在安全研究中,我们使用反射的目的有很多,其中之一就是用于绕过某些沙盒,比如上下文中如果只有Integer类型的数字,那我们如何获取到可以执行命令的Runtime类呢?
1.getClass().forName("java.lang.Runtime")
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 。
第二个参数initialize常常被人误解,比如:
图中有说:”构造函数,初始化时执行”,其实在forName的时候,构造函数并不会执行,即使我们设置initialize=true,那么forName的初始化指的是什么?
可以将这个初始化理解为类的初始化,我们来看下如下类:
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 {} 就是在“类初始化”的时候调用的,而 {} 中的代码会放在构造函数的 TrainPrint() 后面,但在当前构造函数内容的前面。
所以说, forName 中的 initialize=true 其实就是告诉Java虚拟机是否执行”类初始化“。
那么,假设我们有如下函数,其中函数的参数name可控:
public void ref(String name) throws Exception {
Class.forName(name);
}
我们就可以编写一个恶意类,将恶意代码放置在 static {} 中,从⽽执⾏:
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
} }
}
这个恶意类如何带入目标机器中,可能就涉及到ClassLoader的⼀些利用方法了。
正常情况下,除了系统类,如果我们想要拿到一个类,需要先import导入才能使用,而使用forname就不需要,这样对我们攻击者特别有利,可以加载任意类。
另外我们可能会在一些源码中看到类名的部分包含$符号,$的作用是查找内部类。
Java的普通类c1中支持编写内部类c2,而在编译的时候会生成两个文件:c1.class和c1$c2.class,我们可以把他们看作两个不相关的类,通过Class.forName(“c1$c2”)即可加载这个内部类。
获得类以后,我们可以继续使用反射获取这个类中的属性、方法等,也可以实例化这个类,调用方法。
Class.newInstance()的作用是调用这个类的无参构造函数,但是有时候可能会调用失败,可能是以下两个原因:
- 使用的类没有无参构造函数
- 使用的类构造函数是私有的
最常见的情况就是java.lang.Runtime,这个类在我们构造命令执行payload的时候很常见,但我们不能直接这样执行:
Classclazz=Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.newInstance(),"id");
这样会报错:
原因就是因为Runtime类的构造方法是私有的,这里有一个疑问就是如果是私有的构造方法,那用户怎么调用这个类,这里涉及到”单例模式”,以后再说,可以通过下边这个例子理解一下:
对于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支持类的重载,我们不能仅通过一个函数名字就确定一个函数,所以在调用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");
上边说了简单的命令执行payload,但是有两个问题:
- 如果一个类没有无参构造方法,也没有类似单例模式中的静态方法,我们怎么通过反射实例化该类?
- 如果一个方法或构造方法是私有方法,我们是否能执行它呢?
第一个问题,我们需要用到一个新的反射方法 getConstructor:
和getMethod类似,getConstructor方法接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。
获取到构造函数后,我们使用newInstance来执行。
比如,我们常用的另一种执行命令的方式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)
上边用的是第一种情况,所以传入的是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方法,然后invoke执行,invoke的第一个参数就是ProcessBuilder Object。
那么如果我们使用第二种情况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();
再说一下上边提到的第二个问题,如果一个方法或者构造方法是私有方法,我们能否执行它?
这就涉及到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 修改它的作用域,否则仍然不能调用。