介绍
本篇学习java的反射
将介绍通过反射 ,无视作用域,调用对象方法,调用构造函数实例化对象,获取、修改对象私有变量等操作
java代码审计学习(二、反射)
上一篇留下了个问题
如果一个对象或方法,作用域是default或private,当前代码不在作用域内,怎么调用?
在解决问题之前,先来点预备知识:反射
什么是反射
在我理解其实很简单,就是通过字符串去调用函数,变量或类
什么意思?
举个例子
例如本地有getAge、getName、getSex。。。等等,很多函数,用于用于查询个人信息
当用户查询信息时会调用这些方法,例如用户传入age,则调用getAge方法
怎么实现?
笨方法:使用if
if(userin == "age"){
getAge();
}else if(userin == "name"){
getName();
}else .......
有多少函数就写多少if,这不累死
所以,反射就派上用场了:
首先将用户输入转为函数名("get"+首字母大写的userin),然后通过函数名去调用对应函数
怎么用代码实现?
先看下用php如何实现
$userin = "age";
$func = "get".ucfirst($userin); // 将age变为getAge,这里.是字符串拼接,ucfirst是首字母大写
$func();
ok,就这么简单,拼接出函数名,然后就可以将这个字符串变量作为函数调用了,所以上面的问题可以用这句话解决"get".ucfirst($userin)();
java的反射
下面再来看看java怎么实现反射
因为java所有函数都在类里,所以第一步,要获取到类,调用forName方法,通过类名hanhan获取到类对象
再用类对象的getMethod方法,获取到类里的han函数的对象
调用hanhan对象的han方法
不过这里有个疑问invoke为什么要传入clazz.newInstance(),直接调用hanhan类的han方法不行吗
这里newInstance调用了类的无参构造函数创建了hanhan的对象,clazz.newInstance()等价于new hanhan()。为什么要传这个参数?
调试一下,看这个参数最终用来做什么了private static native Object invoke0(Method m, Object obj, Object[] args);
调试发现,最终传给了native方法invoke0,好吧,,看不见native方法的代码,那猜测一下原因
因为这里调用的是"方法",而不是"函数",方法在执行时,会用到类的成员变量或方法,它属于对象,和对象是一个整体,它的执行可能是受对象里的成员变量影响的,而"函数"就是"单打独斗",可以直接调用
例如通过invoke创建数据库对象,连接数据库
可能先创建对象,然后手动设置类的成员变量,如数据库地址、账号密码等
这些都做好后,通过invoke调用这个对象的connect方法,connect方法读取类成员变量,连接数据库
这也是java面向对象编程和面向过程编程的一种区别吧
反射是非常有用的一种机制,它允许我们"动态的"去执行代码。执行什么代码,可以通过用户指定
对比各种语言的反射
c和go这种编译型语言的反射我愿称之为伪反射,它们反射的原理是需要手动建一个映射表,例如age->getAge(),在使用反射时需要查表,新的方法需要用到反射,那就要手动将新函数写到反射表中
而php、python、java等解释型语言的反射,不需要手动去建反射表
猜测因为编译型语言在编译后是汇编代码,运行时直接执行汇编代码,而汇编调用函数时通过地址调用,与函数名无关,所以想通过函数名调用,只能做一个函数名与函数地址的映射表实现反射
而解释型语言运行代码是靠解释器运行的,解释器看一眼代码后,根据它的"理解"去做相应"动作",执行的不是代码本身。相当于代码指挥解释器去做事,这种当然自由度高,当然可以通过函数名调用函数,即反射
回到正题
从上面图中可以看到,clazz.newInstance()方法有删除线
在java9之后推荐使用.getDeclaredConstructor().newInstance()代替.newInstance(),后面会介绍getDeclaredConstructor方法
现在知道了如何通过反射来调用指定类的指定方法
下面再试一下通过反射的方式,实现上一篇中提到的三种命令执行方法
通过反射,再现三种命令执行方式
1、先看下ProcessBuilder方式
public static void main(String []args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Class clazz = Class.forName("java.lang.ProcessBuilder"); // 获取类对象
Method m = clazz.getMethod("start"); // 获取方法对象
Process p = (Process) m.invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("whoami"))); // 创建ProcessBuilder对象,再调用对象的start方法
show(p);
}
分析下这里的clazz.getConstructor(List.class).newInstance(Arrays.asList("whoami"))
getConstructor方法是获取类的构造方法,参数是构造函数的参数类型,然后通过newInstance实例化对象
ProcessBuilder构造方法有两个
public ProcessBuilder(List<String> command);和public ProcessBuilder(String... command);
所以getConstructor(List.class)选择了第一种构造方法
然后newInstance传入List类型数据(List是接口,可以接收列表类型对象)
2、使用runtime
public static void main(String []args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Class clazz = Class.forName("java.lang.Runtime"); // 获取Runtime类对象
Method execMethod = clazz.getMethod("exec", String.class); // 获取exec方法对象
Method getRuntimeMethod = clazz.getMethod("getRuntime"); //获取getRuntime方法对象
Object runtime = getRuntimeMethod.invoke(clazz); // 调用getRuntime方法
Process p = (Process) execMethod.invoke(runtime, "whoami"); //调用exec方法
show(p);
}
又有新知识点,
知识点一
这个步骤比ProcessBuilder方式多了两个:获取getRuntime方法,和调用getRuntime方法
为什么?
因为Runtime类用的是单例模式(设计模式中的一种),构造函数是private Runtime() {}
其他类不能调用构造方法,就没办法创建Runtime对象
那其他类怎么使用Runtime?
Runtime类有一个静态变量private static final Runtime currentRuntime = new Runtime();
这里要讲一下static修饰符
被static修饰符修饰的代码在类初始化时被执行
什么时候类初始化时?当使用到这个类内的东西(变量或方法)时就会初始化
Runtime类还有一个静态方法
public static Runtime getRuntime() {
return currentRuntime;
}
当调用getRuntime时,类开始初始化,就会从上到下执行静态代码,先执行private static final Runtime currentRuntime = new Runtime();
创建了Runtime
对象
然后getRuntime()返回Runtime对象
类只会初始化一次,所以Runtime对象只会被创建一次,,其它代码想获得Runtime对象,只能调用getRuntime()方法
这种单例模式还可以用在数据库类,保证数据库对象只执行一次连接,只有一个数据库对象,对数据库的操作都通过这个对象进行
所以,开始的代码,多出的两个步骤:获取getRuntime方法,和调用getRuntime方法,是为了获取Runtime对象
知识点二
注意这段代码Object runtime = getRuntimeMethod.invoke(clazz); // 调用getRuntime方法
上面学习invoke方法时,invoke传入参数是clazz,而不是该方法的实例
之前不是分析invoke第一个参数应该是该方法的实例对象吗?因为调用方法时,可能会用到对象的成员变量,所以要先创建对象
但这次调用的方法是getRuntime,一个静态方法
静态方法和变量属于类,所有static修饰的变量或方法都会在类加载时执行,静态方法或变量是一个类的所有实例共用的
静态方法不需要实例化就可以调用,所以静态方法调用方法外的方法或变量都必须要是静态的(因为非静态变量或方法需要实例化对象才能用)
所以静态方法,更像是"函数"的概念,写在类里,作用域为当前类的函数
所以
静态方法的invoke不需要传入该方法的实例化对象,但是invoke需要至少一个参数,所以,这个参数可以为任意值,可以为null,12312,"12321"
如图,这样也可以正常执行
3、使用processImpl
上一篇在使用processImpl命令执行时遇到了问题,当前代码不在processImpl的作用域,下面看看如何通过反射解决这个问题
public static void main(String []args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Class clazz = Class.forName("java.lang.ProcessImpl"); // 获取ProcessImpl的Class对象
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class); // 获取start方法对象
method.setAccessible(true); // 设置方法允许在当前代码使用
Process p = (Process) method.invoke(null, new String[]{"whoami"}, null, ".", null, true); // 调用start方法
show(p);
}
可以看到之前的getMethod
函数变为了getDeclaredMethod
getMethod
函数是从public方法中获取方法,包括父类中的public方法getDeclaredMethod
函数是从当前类所有声明的方法中获取方法,不包括父类声明的方法
getXXX系列和getDeclaredXXX系列函数,可以获取Method(方法)、Constructors(构造函数)、Fields(成员变量),区别和上面一样
setAccessible(true)
是修改方法的访问权限,允许当前代码调用
运行一下
如图,命令执行成功,在执行setAccessible(true)
时,java发出了警告
这里invoke第一个参数是null,看一下ProcessImpl的start方法,果然,是静态方法
上面就是通过反射,实现三种命令执行方式
反射的更多用途
Constructor和Fields
上面的例子中用到了getDeclaredMethod
下面再学习下
getXXX系列和getDeclaredXXX系列的Constructor(构造函数)和Fields(成员变量)
就以Runtime为例
上面提到过,Runtime是单例模式,因为构造函数是private,所以只能在加载类时,实例化一个currentRuntime对象,因为这个对象是private,所以之后只能通过getRuntime获取
如图
但是学会反射就可以为所欲为了
构造函数是private?那我可以通过getDeclaredConstructor来调用构造函数
如图
currentRuntime对象是private?
那可以通过getDeclaredFields获取
这里field的get方法,本应传入类对象的,但因为currentRuntime是静态变量,所以get可以传任意值
field还有set方法,可以修改变量值、getType方法,获取变量类型
这里就不演示了
反射获取内部类
示例如图
注意这里使用的内部类的名称为"Student$Family"
为什么用这个名呢?
如图,使用javac对当前java文件编译,每个类都生成对应class文件,内部类Family的命名方式是 外部类$内部类.class
所以,forName方法是通过class文件名来加载Class对象的
之前提到了static修饰符,再补充下知识点
static修饰符
static修饰符修饰的变量或方法会在类加载时调用
static还可以这样用,叫做静态代码块
static int a = 1
static{
a += 1;
}
这里的{}是作用域,在{}内声明的变量是局部变量,作用域为当前大括号,外界不能访问,{}内可以像在函数内一样执行代码
如果去掉static,{}内的代码叫做构造代码块
int a = 1
{
a += 1;
}
静态代码块是初始化类用的,在类初始化时从上到下执行
构造代码块是给对象初始化用的,在对象初始化时,在构造方法之前执行
所以各种代码块相当于被指定了加载顺序,自动被调用的匿名方法
静态代码块在类加载时就会调用
所以可以像下面这样,写一个恶意类,当这个类被加载时,就会命令执行
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
}
}
}
至此,反射篇,结束了
总结
学会了反射简直可以为所欲为,很强大,但使用不当也很危险
而且感觉它不符合面向对象的思想
通过反射,可以无视作用域修饰符,调用方法,实例化对象,调用、修改变量等