前言
这是正式开始学习Java反序列化漏洞的第一篇,而Java反射机制是熟知Java反序列化漏洞的第一步,此系列笔记是为了自己能更好理解反序列化漏洞,也希望通过学习,自己能深层了解漏洞成因、学会利用、自己编写、改编利用工具。
Java Reflection
什么是Java反射机制
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。
通俗的来说,就是我们可以通过Java的反射机制来获取到任意一个Class、变量、method、instance等等,而我们动态地任意获取Class,正有利于我们实现反序列化漏洞的利用。
静态语言和动态特性
简单来说,动态语言可以改变一个变量的类型 - 你不用提前定义某个变量的类型,比如Python和PHP,这些语言会在运行时自动探针你的变量类型,而你也可以在代码中随时对这些变量类型进行改变。
而静态语言例如Java,C/C++,C#就不一样,我们必须事先指定变量类型是String,int,还是double?
而在Java中,有一个反射机制,它可以为我们提供一些动态特性 - 即使Java是一门静态语言。
Java的反射机制可以让我们做到如下:
- 在程序运行时,查找到一个Object所属的Class
- 在程序运行时,能找到任意一个Class的variable和method
- 在程序运行时,可以构造任意一个Class的instance(Object)
- 在程序运行时,可以调用任何一个Object的方法
Class类
先来看一张图:

Class类: 保存类信息的类
每一个新的class在创建的时候,都会new一个新的Class类。
比如,我们在创建一个String类,这个String类在创建的同时,会有一个Class cls=new Class(String)被创建,专门用来保存这个String Class。
再举个例子,比如我们在创建一个Person类,这个Person类在创建的时候,会有一个Class cls = new Class(Person)这样的instance被创建。但Class类和这两个类并非继承关系!!
所以,我们需要区分的是Class类是一个类,就像Person和String类一样,都是类,Person和String类在生成的时候,都会先生成Class类的instance(对象)。
在Class类中,我们含有各种方法函数,其中Class类的构造方法是private,Class类还拥有getMethod,invoke这样的方法。
从图中可以看出:我们可以通过一个【person instance】.getClass()获取到person的Class类对象
通过【person的Class类对象】.class获取到person instance.
利用反射机制获取Class及Class静态初始化
我们知道有三种方法可以获取到一个Class对象
- obj.getClass();- 当我们知道obj instance对象时可以使用这个方法
 
- Class.forName("Class的名字");
- Class.forName( String className , Boolean initialize , ClassLoader loader );- 此处 - Class.forName("Class的名字");等同于- Class.forName( "Class的名字" , true , currentLoader );- String className: 类名
- Boolean initialize: 是否进行类初始化
- ClassLoader loader: 加载器( 告诉 Java 虚拟机如何加载获取的类 , Java 默认根据类名( 即类的绝对路径 , 例如- java.lang.Runtime())来加载类 )
 
- Class在初始化的时候,会自动执行- static{}中的代码,如果我们能够控制一个- Class,并向其中添加含有恶意代码的静态代码块,当- Class被初始化的时候就会执行恶意代码。
- 当我们知道Class名字时可以使用这个方法 
 
- className.class- 当我们已经加载过某个Class,可以用这个方法
 
其中,Class.forName("Class的名字");是最为常用的一种方式。
| 1 | Class<?> person = Class.forName("Person"); | 
利用反射机制获取method
我们在通过上面的代码获取到class后,可以获取到该Class的method。
需要注意的是,我们依然有两种不同的方法来获取到method。
| 1 | //第一种方式: | 
构造instance
 获取到了Class,我们又该如何获取到一个新的object呢?
假设在Person Class中,我们有三个构造函数:
| 1 | public Person(){//无参构造 | 
当我们依次获取时
| 1 | person.newInstance();//在创建新的instance时,class的constructor会被执行,而此处默认执行无参数constructor | 
可得如下结果

invoke方法
当我们构造出来了一个新的instance(Object)且得到需要的method后,我们该如何call这个Class的方法呢?
| 1 | Object o = person.newInstance();//得到object | 
补充
java.lang.Runtime命令执行及访问private的方法
我们在反序列化漏洞中会遇到的java.lang.Runtime是执行命令的最常见的方式。
以下是java执行命令的语句
| 1 | Runtime.getRuntime().exec("ipconfig"); | 
那如果我们要用java的反射机制来执行这段代码应该如何操作呢?
于是我写下了这段代码:
| 1 | Class<?> runTimeClass = Class.forName("java.lang.Runtime");//先找到Runtime这个Class的Class类 | 
但是奇怪的事情发生了:

代码提示19行出错 - 我们不能访问private属性的成员。难道是constructor有问题吗?
我们尝试进入Runtime Class中看一下:
 果然,该构造方法是
 果然,该构造方法是private的,加上前面所提到的,className.newInstance()是直接使用无参构造,所以我们不能直接创建这个instance。 
如何解决呢?我们有两个方法
- getRuntime() - 其实之前我们就有写到执行语句是 - Runtime.getRuntime().exec("ipconfig");此处我们就没有new一个新的- instance,反而是直接用- getter来获取到- Runtime。- 所以我们在此处可以这样写: - 1 
 2
 3
 4
 5- Class<?> runTimeClass = Class.forName("java.lang.Runtime");//先找到Runtime这个Class 
 Method exec = runTimeClass.getDeclaredMethod("exec", String.class);//找到exec这个method
 Method getRuntime = runTimeClass.getDeclaredMethod("getRuntime");//找到getRuntime()这个method
 Object o1=getRuntime.invoke(null);//call getRuntime()来获取到Runtime实例
 exec.invoke(o1,"ipconfig");//执行方法
- setAccessible() - 访问private方法 - Java会对- private的方法进行禁止访问的操作,而- setAccessible可以让我们禁止或允许- Java语言的访问检查,当我们设置- setAccessible(true)时,我们便可以访问- private方法。- 1 
 2
 3
 4
 5
 6- Class<?> runTimeClass = Class.forName("java.lang.Runtime");//先找到Runtime这个Class 
 Method exec = runTimeClass.getDeclaredMethod("exec", String.class);//找到exec这个method
 Constructor<?> declaredConstructor = runTimeClass.getDeclaredConstructor();//不带参数的getConstructor是会获取到无参构造方法的,但因为Runtime的Constructor是private的,所以我们需要使用Declared
 declaredConstructor.setAccessible(true);//禁止java语言访问检查,让我们可以访问这个私有的constructor
 Object o1 = declaredConstructor.newInstance();//通过该constructor来创建新的instance
 exec.invoke(o1,"ipconfig");//完成命令执行
java.lang.ProcessBuilder命令执行
知道我们是通过exec()执行代码后,我们接下来想要弄清楚,exec()是怎么工作的。在尝试跟随exec()方法后,我们最终来到了如下地方:

可见我们创建了一个ProcessBuilder的instance,同时向其中传入了我们需要执行的命令cmdArray,cmdArray也是以String[]的类型传入的。
进入Processbuilder可以看到它有两个constructor,一个是传入有参数的(List<String>类型),一个是传入无参数的(String[]类型),注意,这里String...等价于String[]

再看start()方法

传入的command又会被转化为Array,最后开始创建子程序执行等等(就不深入探究了)
综上所述,我们只需要执行new ProcessBuilder.start()即可完成命令执行
于是我们写出如下代码:
| 1 | Class<?> processBuilderClass = Class.forName("java.lang.ProcessBuilder");//获取到ProcessBuilder这个class | 

Conclusion
- Java反射机制是Java这个静态语言中的动态特性,它让我们能够获取到、执行任意类的方法、变量,也能创建任意类的对象。 
- Class被初始化时会自动执行- static{}中的内容,此处可以被利用。
- Declared关键字让我们可以获取处继承类以外的所有方法。
- setAccessible(true)可以访问private方法,不被- java语言进行访问检测。
- 我们可以通过Java反射机制利用Runtime和ProcessBuilder执行命令 
