文章目录
- 写在前面
- 什么是java的反射机制?
- 通过Singleton获取实例
- Runtime类
- 利用反射机制获取类对象
- 利用反射机制获取函数
- 执行方法
- 小结
- 通过Constructor获取实例
- 坑
- 私有方法的调用
- 更灵活的调用方式
- 小结
- Reference
写在前面
本文是学习了 啦啦菌NODE 大神的文章 Java反射机制 后根据自己的理解进行的记录和整理,希望拜读原文的请大家移步大神的博客。
什么是java的反射机制?
在Java中的反射机制是指在运行状态中,**任意一个类都,能知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法。这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。**简单来说,反射机制指的是程序在运行时能够获取自身的信息。在 Java 中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。1
通过Singleton获取实例
Runtime类
在每一个JVM进程里面都会存在有一个Runtime类的对象,这个类的主要功能是取得一些与运行时有关的环境属性,或者创建新的进程.
直接调用的方式为Runtime.getRuntime().exec("open -a Calculator")
,这条语句调用本机的计算器程序,并创建进程。
验证poc的时候通常会调起一个系统计算器进程,
open -a Calculator
是mac在terminal中打开计算器的命令,windows中通常为calc.exe
上述语句,如果采用反射机制根据类的名字和方法的名字实现,应该怎样编写呢?
- 利用反射机制获取Runtime类;
- 利用反射机制获取exec这个方法
- 传入参数并执行exec方法
首先我们来解决第一个问题
利用反射机制获取类对象
获取java.lang.class
类对象的三种方法
obj.getClass()
:如果上下文中存在某个类的实例obj,那么我们可以直接通过obj.getClass()
获取它的类。
- obj是实例对象:通过
obj.getClass()
可以获取该实例的类对象。(如Runtime.getRuntime().getClass()
的结果是class java.lang.Runtime
类) - obj是类对象:通过
obj.getClass()
可以获取java.lang.class
类
obj.class
:当obj是一个已经加载的类时,直接通过.class
参数获取它的java.lang.Class
对象。Class.forName
:如果知道类的名字,可以直接使用forname来获取java.lang.Class
初始化对象。
* A call to {@code forName("X")} causes the class named * {@code X} to be initialized.
很明显,我们是要根据类的名字获取类对象,因此这里要用到Class.forName()
的方式。
Class.forName
有两种调用方式
Class<?> forName(String className)
Class<?> forName(String name, boolean initialize, ClassLoader loader)
前一种调用其实是后面的封装, 下面这两种调用是等价的
Class.forName("Foo") Class.forName("Foo", true, this.getClass().getClassLoader()
第一个问题到这里就有答案了,我们可以通过
Class<?> runtimeCls = Class.forName("java.lang.Runtime");
来获取Runtime类。
利用反射机制获取函数
getMethod(String name, Class<?>... parameterTypes)
是通过反射获取某个类的public方法。
/**
* Returns a {@code Method} object that reflects the specified public
* member method of the class or interface represented by this
* {@code Class} object. The {@code name} parameter is a
* {@code String} specifying the simple name of the desired method. The
* {@code parameterTypes} parameter is an array of {@code Class}
* objects that identify the method's formal parameter types, in declared
* order. If {@code parameterTypes} is {@code null}, it is
* treated as if it were an empty array.
*/
因此在获取类对象后,可以进一步使用getMethod
方法来获取类对象的函数。这里我们使用getMethod来获取Runtime的exec方法。exec的参数应为字符类型,即String.class
,因此可以以如下方式获取exec方法。
Method exec = runtimeCls.getMethod("exec", String.class);
执行方法
getMethod
返回的是一个Method
对象,而Method
类下存在方法invoke()
,它的作用是传入参数,执行方法。
从invoke的注释中,可以了解到以下信息。
public Object invoke(Object obj, Object... args)
@param obj
: 执行方法的类对象实例。
- 如果要执行的方法时普通方法,传入调用这个方法的类对象实例
- 如果要执行的方法是静态方法,那么obj可以忽略,传入Null即可。
- 如果要执行的方法是静态方法,且类对象还未初始化,那么在调用invoke的时候会执行初始化。
-
@param args
: 执行方法所需的参数。如果没有参数的话,args可以为长度0的Array或者Null。
这里我们要执行的exec是一个普通方法。因此第一个参数需要传入执行exec的类象实例,即Runtime
实例。根据前面所学,我们可以用Class.forName("java.lang.Runtime")
获取类对象,但是需要注意的是这里仅仅是获取了类对象,而不是实例。如果以这种方式构造成后面介绍的完整语句并且调用,会报错object is not an instance of declaring class
。因此获取类对象后要继续获取类实例。Runtime可以用静态方法getRuntime()
获取Runtime的实例,这个函数也不需要参数,因此我们可以用getmethod配合invoke来获取Runtime的实例, 也就是
Object runtimeObj = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
第二个参数传入exec要执行的命令,即open -a Calculator
。
String execParam = "open -a Calculator";
小结
综上,我们可以以如下方式利用反射机制使用runtime的exec方法启动一个进程
// 获取exec方法
Class<?> runtimeCls = Class.forName("java.lang.Runtime");
Method exec = runtimeCls.getMethod("exec", String.class);
// 利用getmethod获取runtime实例
Method getMethod = runtimeCls.getMethod("getRuntime");
Object runtimeObj = getMethod.invoke(null);
// exec参数
String execParam = "open -a Calculator";
// 执行exec
exec.invoke(runtimeObj, execParam);
如果写成一行语句的话,
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"open -a Calculator");
以上我们完成了通过类内置的静态方法获取类的实例,进一步调用一个public方法。但是假如一个类没有无参构造方法(即不能class.newInstance()),也没有单例模式(只存在一个实例)的静态方法(即不能像getRuntime一样获取实例),那我们该如何实例化这个类呢?
通过Constructor获取实例
ProcessBuilder
用来创建操作系统的进程。可以通过以下方式调用一个进程
List<String> paramList = new ArrayList<>();
paramList.add("open");
paramList.add("-a");
paramList.add("Calculator");
ProcessBuilder pb = new ProcessBuilder(paramList);
pb.start();
Constructor<T> getConstructor(Class<?>... parameterTypes)
函数反射类的Constructor对象,然后利用Constructor.newInstance(Object ... initargs)
方法获取实例化的对象。
newInstance()
返回的是T对象,因此要执行ProcessBuilder的方法还需要进行一次强制类型转化。
((ProcessBuilder) Class.forName("java.lang.ProcessBuilder").getConstructor(List.class).newInstance(Arrays.asList("open","-a","Calculator"))).start();
进一步利用上面getMethod的方式调用start方法,
Class.forName("java.lang.ProcessBuilder").getMethod("start").invoke(Class.forName("java.lang.ProcessBuilder").getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
成功调起计算器进程。
ProcessBuilder除了上述的Constructor外,还有另外一个Constructor
ProcessBuilder(String... command)
以这种方式调用进程的方式是
ProcessBuilder pb = new ProcessBuilder("open", "-a", "Calculator");
pb.start();
传入的参数类型不是List<String>
而是String...
,即String[]
,因此采用这种Constructor的时候,getConstructor()
的参数应为String[].class
。
最终构造的语句为
Class.forName("java.lang.ProcessBuilder").getMethod("start").invoke(Class.forName("java.lang.ProcessBuilder").getConstructor(String[].class).newInstance((Object[])new String[][]{{"open", "-a", "Calculator"}}));
坑
注意到
newInstance()
中传入的参数是(Object[]) String [][]
类型的,并不是String[]
。这是为什么呢?一开始尝试传入
String[]{"open", "-a", "Calculator"}
,报错java.lang.IllegalArgumentException: wrong number of arguments
。于是调试比较两个Constructor在newInstance时传入的参数有何不同。下图是List.class时传入的参数
下图是String[].class时传入的参数[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y8vLntIG-1639315281205)(Img/image-20211212145333040.png)]
可以注意到传递到newInstance的时候,少了一层,所以构建的时候在外面添加一层列表new String[][]{{"open", "-a", "Calculator"}}
。
但是这样传递发现还是报错
java.lang.IllegalArgumentException: argument type mismatch
。注意到编译器有warning,Type String[][] of the last argument to method newInstance(Object...) doesn't exactly match the vararg parameter type. Cast to Object[] to confirm the non-varargs invocation, or pass individual arguments of type Object for a varargs invocation.
于是把添加强制类型转化为
(Object[])new String[][]{{"open", "-a", "Calculator"}}
。可以成功运行
私有方法的调用
上面两个例子都是调用的Public方法,如果要获取private方法呢?
前面我们使用了getMethod
来获取public方法,getConstructor
来获取构造器,下面来学习getDeclaredmethod
和getDeclaredConstructor
两个方法。这两个方法获取的是当前类中声明的方法,包扩所有私有的方法但不包括从父类继承的方法。
还以Runtime为例,Runtime的构造方法是一个private方法。我们利用getDeclaredConstructor
获取它的constructor并调用一个计算器进程。
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getDeclaredConstructor().newInstance(), "open -a Calculator");
运行上述语句,得到一个报错
java.lang.IllegalAccessException: Class RuntimeExec2 can not access a member of class java.lang.Runtime with modifiers "private"
虽然得到了私有方法,但是还需要进一步打破对私有方法的调用,这里使用setAccsessible(true)
来打破访问限制。
Constructor<?> con = Class.forName("java.lang.Runtime").getDeclaredConstructor();
con.setAccessible(true);
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(con.newInstance(), "open -a Calculator");
运行上述语句成功调起计算器。
更灵活的调用方式
如果不允许使用Class.forName("java.lang.Runtime").getMethod()
,这个时候怎么办呢?
可以使用反射机制来获取getMethod方法。
Class.forName("java.lang.Runtime").getMethod("getRuntime")
Class.forName("java.lang.Class").getMethod("getMethod", new Class[] {String, Class[].class}).invoke(Class.forName("java.lang.Runtime"), "getRuntime", new Class[0]);
小结
到此为止,我们已经学习了如何利用反射机制调用任意类的任意方法。