反射 (Reflection) 是 Java 的特征之一,在C/C++中是没有反射的,反射的存在使得运行中的 Java 程序能够获取自身的信息,并且可以操作类或对象的内部属性。那么什么是反射呢?
对此, Oracle 官方有着相关解释:
“Reflection enables Java code to discover information about the
fields, methods and constructors of loaded classes, and to use
reflected fields, methods, and constructors to operate on their
underlying counterparts, within security restrictions.”
(反射使Java代码能够发现有关已加载类的字段、方法和构造函数的信息,并在安全限制内使用反射的字段、方法和构造函数对其底层对应的对象进行操作。)
简单来说,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。同样的,JAVA的反射机制也是如此,在运行状态中,通过 Java 的反射机制,对于任意一个类,我们都能够判断一个对象所属的类;对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
反射的用途很广泛,如在我们开发使用到Eclipse、IDEA等开发工具的时候,当我们输入一个对象或类并想调用它的属性或方法时,编译器就会自动列出它的属性或方法,这里用到的便是反射;再如,javaBean和jsp之间的调用,也用到了反射。这些不是反射最主要的用法,反射最重要的用途其实是开发各种通用框架,如我们上文中提到的Spring框架以及ORM框架,都是通过反射机制来实现的。
反射这个机制其实很有意思,面向不同的人员,其重要程度也大不相同。对与框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点。但总的来说,适当了解框架的底层机制对我们的编程思想,也是非常有帮助的。
由于Java的大部分应用框架都采用了反射机制,因此掌握好Java反射机制对于我们的代码审计能力有很大的帮助。
获取类的对象有很多种,这里提供四种方式:
forName()
方法如果要使用Class类中的方法完成,就需要用forName()
方法,这种方式只要有类名称即可,更为方便,扩展性更强。如下图所示为获取类对象的示例:
这种方法其实我们并不陌生,在配置 JDBC 的时候,我们通常这么写,如下图所示:
任何数据类型都具备一个静态的属性,可以使用.class
来获取其对应的Class对象。相对简单,但是还是要明确用到类中的静态成员,如下图所示:
getClass()
方法我们可以通过 Object 类中的 getClass()
方法来获取字节码对象,不过这种方式较为繁琐,必须要明确具体的类,然后创建对象,如下图所示:
getSystemClassLoader().loadClass()
方法getSystemClassLoader().loadClass()
方法和 forName()
方法类似,这种方式也是只要有类名称即可,但与 forName()
方法还是有些区别,forName()
的静态方法 JVM 会装载类,并且执行 static { }
中的代码,而 getSystemClassLoader().loadClass()
不会执行static()的代码,如在上文中提到的使用 JDBC时,就是利用 forName()
方法,让 JVM 查找并加载指定的类到内存中,此时将"com.mysql.jdbc.Driver
" 当做参数传入,就是告诉JVM,去"com.mysql.jdbc
" 这个路径下找 Driver 类,将其加载到内存中。该方法具体如下图所示:
获取某个Class对象的方法集合,主要有以下几个方法:
getDeclaredMethods()
方法getDeclaredMethods()
方法返回类或接口声明的所有方法,包括:public、protected、private和默认方法,但不包括继承的方法,具体方式如下图所示:
第二种:getMethods()
方法
getMethods()
方法返回某个类的所有public方法,包括其继承类的public方法,具体方式如下图所示:
getMethod()
方法getMethod()
方法只能返回一个特定的方法,如 Runtime 类中的exec()
方法,该方法的第一个参数为方法名称,后面的参数为方法的参数对应Class的对象,具体如下图所示:
getDeclaredMethod()
方法getDeclaredMethod()
方法和getMethod()
类似,它也只能返回一个特定的方法,该方法的第一个参数为方法名,第二个参数名是方法参数,具体如图所示:
为了更直观的体现出获取类成员变量的方法,我们首先创建一个 Student 类,如下图所示:
如果我们想获取这个 Student 类成员变量,那么主要有以下几个方法:
getDeclaredFields()
方法getDeclaredFields()
方法能够获得类的成员变量数组,包括public、private和proteced,但是不包括父类的申明字段。具体方式如下图所示:
getFields()
方法getFields()
能够获得某个类的所有的public的字段,包括父类中的字段,具体如下图所示:
getDeclaredField()
方法该方法与getDeclaredFields()
区别是这个方法只能获得类的单个成员变量,如我们仅想获得Student 类中的name 变量,那么具体如下图所示:
getField()
方法与getFields()
类似,getField()
方法能够获得某个类的特定的public的字段,包括父类中的字段,如我们想获得 Student 类中的 public类型变量content,具体如下图所示:
在上文中我们提到过,利用Java的反射机制,我们可以无视类方法、变量访问权限修饰符,可以调用任何类的任意方法、访问并修改成员变量值,那么这可能导致安全问题,如果一个攻击者能够通过应用程序创建意外的控制流路径,那么就有可能绕过安全检查发起相关攻击。假设有段代码如下:
String name = request.getParameter("name");
Command command = null;
if (name.equals("Delect")) {
command = new DelectCommand();
} else if (ctl.equals("Add")) {
command = new AddCommand();
} else {
...
}
command.doAction(request);
存在一个字段为name,当获取用户请求的name字段后进行判断,如果请求的是 Delect 操作,则执行DelectCommand 函数,若执行的是 Add 操作,则执行 AddCommand 函数,如果不是这两种操作,则执行其他代码。
此时,假如有位开发者看到了这段代码,他觉得可以使用Java 的反射来重构此代码以减少代码行,如下所示:
String name = request.getParameter("name");
Class ComandClass = Class.forName(name + "Command");
Command command = (Command) CommandClass.newInstance();
command.doAction(request);
这样的重构看起来使得代码行减少,消除了if/else块,而且可以在不修改命令分派器的情况下添加新的命令类型,但是如果没有对传入进来的name字段进行限制,那么我们就能实例化实现Command接口的任何对象,从而导致安全问题。实际上,攻击者甚至不局限于本例中的Command接口对象,而是使用任何其他对象来实现,如调用系统中任何对象的默认构造函数,再如调用Runtime对象去执行系统命令,这就可能导致远程命令执行漏洞,因此不安全的反射的危害性极大,也是我们审计过程中重点关注的内容。
其实关于 Java 反射的内容,互联网上很多,也更加具体,有兴趣的朋友可以具体去搜搜看,我相信对我们的审计能力还是很有帮助的。