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 总是不成功,这时候原因可能是:

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

 

最最最常见的情况就是 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. 如果这个方法是一个普通方法,那么第一个参数是类对象
  2. 如果这个方法是一个静态方法,那么第一个参数是类

这也比较好理解了,我们正常执行方法是 [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 修改它的作用域,否则仍然不能调用。

posted @ 2021-04-26 23:03  Punished  阅读(791)  评论(0编辑  收藏  举报