java反射和反序列化
java反射初探
Java安全可以从反序列化漏洞开始说起,反序列化漏洞⼜可以从反射开始说起。
反射是⼤多数语⾔⾥都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有 ⽅法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将Java这种静态语⾔附加上动态 特性。
反射中几个极为重要的方法:
- 获取类的⽅法: 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)
1 Class.forName(className) 2 // 等于 像是malloc和virtualAlloc的区别 3 Class.forName(className, true, currentLoader)
默认情况下, forName 的第⼀个参数是类名;第⼆个参数表示是否初始化;第三个参数就 是 ClassLoader 。
ClassLoader 是什么呢?它就是⼀个“加载器”,告诉Java虚拟机如何加载这个类。Java默认的 ClassLoader 就是根据类名来加载类, 这个类名是类完整路径,如 java.lang.Runtime
第二个参数initialize值得是类的初始化,static方法已经执行,因为static方法随着类的加载而加载,但构造函数并没有执行
所以说, forName 中的 initialize=true 其实就是告诉Java虚拟机是否执⾏”类初始化“。
那么,假设我们有如下函数,其中函数的参数name可控:
public void ref(String name) throws Exception { Class.forName(name); }
我们就可以编写⼀个恶意类,将恶意代码放置在 static {} 中,从⽽执⾏,弹出一个notepad
static { try { Runtime rt = Runtime.getRuntime(); //String[] commands = {"touch", "/tmp/success"}; String[] commands = {"notepad.exe"}; Process pc = rt.exec(commands); pc.waitFor(); } catch (Exception e) { // do nothing } }
java反射二次探索
在正常情况下,除了系统类,如果我们想拿到一个类,需要先 import 才能使用。而使用forName就不 需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。
Java的普通类 C1 中支持编写内部类 C2 ,而在编译的时候,会生成两个文件: C1.class 和 C1$C2.class ,我们可以把他们看作两个无关的类,通过 Class.forName("C1$C2") 即可加载这个内部类。
创造一个内部类:
1 public class Test { 2 public static void main(String[] args) { 3 Test Atest = new Test(); 4 Atest.new TestIn().Print(); 5 } 6 class TestIn 7 { 8 private int i; 9 private String name; 10 11 void Print() 12 { 13 System.out.println("Hello"); 14 } 15 } 16 }
编译后:
获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方 法。
class.newInstance() 的作用就是调用这个类的无参构造函数,这个比较好理解。不过,我们有时候 在写漏洞利用方法的时候,会发现使用 newInstance 总是不成功,这时候原因可能是:
- 你使用的类没有无参构造函数
- 你使用的类构造函数是私有的
最最最常见的情况就是 java.lang.Runtime ,这个类在我们构造命令执行Payload的时候很常见,但 我们不能直接这样来执行命令:
1 Class clazz = Class.forName("java.lang.Runtime"); 2 clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
得到如下错误
因为他的构造方法是私有的:Class reflection.Source can not access a member of class java.lang.Runtime with modifiers "private"
这是因为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分解一下就是:
1 Class clazz = Class.forName("java.lang.Runtime"); 2 Method execMethod = clazz.getMethod("exec", String.class); 3 Method getRuntimeMethod = clazz.getMethod("getRuntime"); 4 Object runtime = getRuntimeMethod.invoke(clazz); 5 execMethod.invoke(runtime, "calc.exe");
- 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类 呢?
- 如果一个方法或构造方法是私有方法,我们是否能执行它呢?
java反射三探
第一个问题,我们需要用到一个新的反射方法 getConstructor 。
和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。
获取到构造函数后,我们使用 newInstance 来执行。
比如,我们常用的另一种执行命令的方式ProcessBuilder,我们使用反射来获取其构造函数,然后调用 start() 来执行命令:
Class clazz = Class.forName("java.lang.ProcessBuilder"); ((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).star t();
解析一下上面的代码:
首先通过类名获得java.lang.ProcessBuilder这个类,再调用getConstructor获得ProcessBuilder的构造器,因为他的构造器有两个,并且得到的是上面这一个构造器
所以参数是List的类,获取到构造器后传参Arrays.asList("calc.exe")用newInstance执行,然后start()
我上面用到了第一个形式的构造函数,所以我在 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了。
那么,如果我们要使用 public ProcessBuilder(String... command) 这个构造函数,需要怎样用反 射执行呢?
这又涉及到Java里的可变长参数(varargs)了。正如其他语言一样,Java也支持可变长参数,就是当你 定义函数的时候不确定参数数量的时候,可以使用 ... 这样的语法来表示“这个函数的参数个数是可变 的”。
对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价 的(也就不能重载):
public void hello(String[] names) {} public void hello(String...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 修改它的作用域,否则仍然不能调用。