JavaSec-反射机制
学习大纲
1 | 1. 反射的定义 |
反射的定义
Java反射机制是在运行状态时,对于任意一个类,都可以获取到这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取信息以及调用对象的方法的功能就称为Java语言的反射机制。
简单来说就是我们可以通过Java的反射机制可以获取到任意类的成员方法、变量,同时可以创建Java类的实例,调用类的任意方法。总而言之我们可以通过反射将Java这种静态语言附加上动态特性。
动态特性
而根据阅读p牛的Java安全漫谈我们得知动态特性即为一段代码,改变其中的变量可以使得这段代码发生功能性的变化;其中最为基础的就是php的一句话木马(php语言具有很多动态特性)
1 | eval($_POST[cmd]); @ |
此时我们可以通过往参数cmd
里面传递不同的值来肆意的执行各种方法以达到我们要的效果;而这正是引起代码功能性的变化;而这种可以被歧义的”动态”也引起了很多的安全问题,这里就不详述了。而java,虽然不像PHP那样灵活,但还是能提供许多动态的特性,也导致了一系列的安全问题。
为什么要使用反射方法
接下来我会用一段代码来告诉大家为社么要使用反射机制,大致流程如下:先引入一段没有动态特性的代码,但是该代码的功能是可以调用其他类的方法和属性,接着使其具有动态特性且代码功能不变。观察在没有引入反射方法时增加类所需要增加的代码量。
没有动态特性
此时我们通过一个例子来引入问什么要引用反射机制
我们此时定义一个getshell
的接口;接口为attack
1 | //定义一个可以getshell的接口 |
因为getshell
有许多方法;我们此时可以实现以下不同方法的getshll
1 | //使用不同的方法来getshell |
那么此时我们在主类中的attack
就可以这么写
1 | public class dream{ |
但是此时没有动态特性(没有办法通过传参进行改变代码特性)
实现动态特性
此时我们可以加入if
语句来实现
1 | class getshell_function{ |
此时我们就可以传入我们想要getshell的参数来获得这个实例;即是我们要用的时候先使getshell_function
拿到实例getshell f = getshell_function.getInstance("exp")
。
整体代码如下
1 | //定义一个可以getshell的接口 |
此时我们就实现了通过传参来改变代码的整体功能实现了所谓的动态特性。
增加其他类时所导致的问题
但是此时的问题也随之而来,当我们需要增加getshell
的操作时会十分的麻烦,此时我们每增加一种方法都要增加三个步骤:
- 1、增加新的
getshell
类型 - 2、增加
if
判断 - 3、主类中调用
而反射就可以帮助我们解决这个问题。我们再次回头看一下反射的定义:Java反射机制是在运行状态时,对于任意一个类,都可以获取到这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取信息以及调用对象的方法的功能就称为Java语言的反射机制。
调用反射的优点
forName()
Class.forName:如果你知道某个类的名字想要调用这个类就可以使用forName来获取
假设我在上面的代码中想要获得exp
这个类,就不需要再使用f = new exp()
;此时我可以直接使用反射**getshell f = (getshell)Class.forName(“exp”).newInstance();**此时我们就可以获取到exp
的实例。那么此时完整的代码就是:
1 | //定义一个可以getshell的接口 |
没有调用forName()
1 | class getshell_function{ |
调用forName()
1 | class getshell_function{ |
此时我们可以看到当我们想增加另外的方法时就不需要再加入其他代码了,只需要利用forName进行获取即可。至此,我们反射获取类的⽅法:**forName()**的意义大概就有了
反射机制
在正常情况下我们想使用一个非系统类时,我们都需要先使用import
才可以使用;而使用forName却不用,我们可以加载任意类,这对我们攻击者十分有利;我们可以通过精心构造的恶意代码进行攻击。
p牛提供给我们一个经典的代码;代码中包含了几个反射中重要的方法
1 | public void execute(String className, String methodName) throws Exception { |
上⾯的例⼦中,p牛演示了几个在反射中重要的方法
- 获取类的方法: forName
- 实例化类的对象方法: newInstance
- 获取函数的方法: getMethod
- 执行函数的方法: invoke
此时我们引入我们常说的弹计算器的payload
来聊聊这个反射
1 | Runtime.getRuntime().exec("calc.exe") |
getRuntime其实就是Runtime类获取对象的方式,等于new一个Runtime类。之所以封装成一个函数是为了不调用一次建立一个对象,只获取一个对象来执行操作。
exec():调用exec函数
calc.exe:调用计算器程序
此时我们将p神给出的经典代码和我们的payload
进行融合看看两者会碰撞出什么样的火花
1 | import java.lang.reflect.InvocationTargetException; |
此时我们一步一步的来聊一聊这个poc
为什么可以成功
forName()
获得一个**class对象(java.lang.Class)**有三种方法:
obj.getClass()
如果在上下文中存在某个类的实例obj
,那么我们可以直接通过obj.getClass
来获取它的类
Test.class-不是反射机制
Test
是一个已经加载的类,想获取它的java.lang.Class
对象直接拿取class
参数即可
Class.forName
如果知道类的名字,可以直接使用forName
来获取
所以此时我们利用Class.forName来获取Runtime类的方法
1 | Class clazz = Class.forName("java.lang.Runtime") |
newInstance()
newInstance的作用是实例化类对象的方法。
getMethod()
getMethod()的作用是通过反射获取一个类的某个特定的公有方法。即为你要调用的函数。而Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,我们需要传给他你需要获取的函数的参数类型列表,后面的String.class是参数。
1 | Class.forName("Java.lang.Runtime").getMethod("exec",String.class) |
invoke()
invoke()方法位于Method类下,其作用是传入参数,执行方法
1 | public Object invoke(Object obj,Object... args) |
此时的第一个参数是执行method
的对象:
1.如果方法是一个普通方法,那么第一个参数是类对象
2.如果方法是一个静态方法,那么第一个参数是类
我们正常执行方法是 [1].method([2], [3], [4]…)
而在反射中是method.invoke([1], [2], [3], [4]…)
正常执行方法:[1].method([2], [3], [4]…)
1 | public class normal_exec { |
毫无疑问,它能正常弹出计算器,运用的原理是java
中存在一个共有类java.lang.Runtime
类,这个实例存在于每个Java
应用程序中,它允许应用程序与允许运行应用程序的环境进行交互。当前的运行可以从getRuntime
方法中获得,但应用程序无法创建自己的此类实例,因此我们想执行命令时,需要先使用该类的主要方法getRuntime
,它可以让我们得到一个和当前程序相关联的Runtime
类的对象,因为大多数Runtime
类的方法是实例方法,所以必须被当前运行时对象调用,只有返回与当前Java
应用相关联的runtime
对象才能使用其中的实例方法。
这其中Runtime对象可以调用exec()方法执行命令,官方文档是这样描述的:在一个单独的进程中执行指定的命令。因此我们执行命令的形式是Runtime.getRuntime().exec("calc.exe");
,先用Runtime.getRuntime()获取类的对象,然后我们就可以调用其中的实例方法比如exec(command),然后输入参数就可以执行各种命令。
利用反射执行方法:method.invoke([1], [2], [3], [4]…)
1 | import java.lang.reflect.InvocationTargetException; |
反射的基本运用
基本流程
此时先使用forName方法获取类中的所有属性包括类名
1 | Class.forName(classname) //获取classname类中的所有属性 |
书接上文,我们将Runtime类中的所有属性赋值给了clazz类,接下来我们想调用clazz类中的某个方法就需要三步:
1 | 1、对类进行实例化(实例化对象) |
第一步是实例化对象,此时就引入了**newInstance()**方法。
对该方法进行简单解释,此方法可以实例化对象,并触发类的构造方法,所以此时的话对象就创建完成了,接下来就是获取方法了。
我们在获取完对象后,对对象中的Public方法获取的方式是采用**getMethod()**函数,这个函数的具体参数如下
1 | getMethod("方法名,参数类型(如String.class)") |
involve可以执行方法,如果是一个普通方法,则involve
的第一个参数为该方法所在的对象,如果是静态方法则第一个参数是Null
或者该方法所在的类,第二个参数为要执行方法的参数。
接下来详细的聊一聊这几个方法。
一、获取类
1.forName()方法
此时只要求我们知道类名即可调用
1 | public class test{ |
2.getSystemClassLoader().loadClass()方法
这个方法与forName
相似,只需要有类名就可以,但是区别在于:forName
的静态JVM会装载类,并执行static()
中的代码
1 | public class getSystemClassLoader{ |
3.使用.class
直接获取
1 | public class test1{ |
4.getClass()方法
obj.getClass()
如果上下文中存在某个类的实例obj
,那么此时我们可以通过obj.Class()
来获取它的类
1 | public class test1{ |
二、获取类的方法
1.getDeclareMethods
getDeclaredMethods:返回类或者接口声明的所有方法,包括public、protected、private
和默认方法,但是不包括继承的方法
1 | import java.lang.reflect.Method; |
2.getDeclaredMethod
getDeclaredMethod:获取特定的方法,第一个参数是方法名,第二个参数是该方法的参数对应的class对象。例如这里Runtime的exec方法参数为一个String,所以这里的第二个参数是String.class
1 | import java.lang.reflect.Method; |
3.getMethods
返回某个类所有的public方法,包括继承类的public方法
4.getMethod
参数同理getDeclaredMethod
三、获取构造函数ConStructor
1 | Constructor<?>[] getConstructors() :只返回public构造函数 |
后面两个方法的参数是对于方法的参数的类型的class对象,和Method的那个类似,例如String.class
demo
1 | String name = "hey"; |
此时如果调用静态方法
1 | // 获取Integer.parseInt(String)方法,参数为String: |
四、反射创建类对象
1 | Class.forName(classname): 获取classname类中的所有属性 |
书接上文,我们在示例中将Runtime
类中的所有属性赋值给了clazz
类,接下来我们想要调用clazz类中的某个方法的话,需要三步
1 | 1、对类进行实例化(实例化对象) |
newInstance()
可以通过反射来实例化对象,一般我们使用Class对象的newInstance()方法进行创建类对象。此时创建的方法为:只需要通过forname()方法获取到的class对象中进行newInstance方法创建即可。
1 | Class c = Class.forName("com.reflect.MethodTest"); //创建Class对象 |
对该方法进行简单解释,此方法可以实例化对象,并触发类的构造方法,所以此时的话对象就创建完成了,接下来就是获取方法了。获取方法的方式我们已经在二中提及这里不在赘诉即为我们在获取完对象后,对对象中的Public方法获取的方式是采用**getMethod()**函数
1 | getMethod("方法名,参数类型(如String.class)") |
此时就可以获取到方法了,接下来我们只需要进行执行方法即可,此时也就引入了我们的最后一个函数invoke。
invoke()
invoke()方法位于java.lang.reflect.Method类中,用于执行某个对象和方法;一般和getMethod方法配合调用
1 | public Object invoke(Object obj,Object... args) |
1.如果方法是一个普通方法,那么第一个参数是类对象
2.如果方法是一个静态方法,那么第一个参数是类
我们正常执行方法是 [1].method([2], [3], [4]…)
而在反射中是method.invoke([1], [2], [3], [4]…)
再聊聊合并反射函数弹出计算器
此时我们继续聊一聊这个弹出计算器的poc
;此时通过反射来弹出计算器的一句话代码如下:
1 | Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),"calc.exe") |
我们将其拆分为更为直观的方法:
1 | import java.lang.reflect.InvocationTargetException; |
此时我们进行一步步的解析来看看这一整个反射的过程
1 | Class clazz = Class.forName("java.lang.Runtime"); |
反射过程
1.初始化java.lang.Runtime
类
此时我们先使用Class clazz = Class.forName("java.lang.Runtime")
来对Runtime
类的初始化;即为获取这个类
1 | Class clazz = Class.forName("java.lang.RUntime") //初始化类 |
2.获取exec
方法
此时我们利用getMethod("exec", String.class)
函数来获取Runtime类中的exec方法
1 | Method execMethod = clazz.getMethod("exec",String.class) //获取exec方法 |
3.获取getRuntime
方法
此时继续利用getMethod("getRuntime")
来获取getRuntime方法。Runtime类
就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对象
1 | Method getRuntimeMethod = clazz.getMethod("getRuntime") //获取getRuntime方法 |
4.获取Runtime
对象
此时继续利用getRuntimeMethod.invoke(clazz);
来获取Runtime对象
1 | Object runtime = getRuntimeMethod.invoke(clazz); //获取RUntime对象 |
5.使用invoke
执行runtime
对象里的exec
方法
execMethod.invoke(runtime, "calc.exe")
1 | execMethod.invoke(runtimr,"calc.exe") //使用invoke执行runtime对象里面的exec方法 |
所以综上所诉在下面这个代码中
1 | Class clazz = Class.forName("java.lang.Runtime"); |
在我们经过五步的拆分之后可以得到一个直观过程
1 | Class clazz = Class.forName("java.lang.Runtime") //获取Runtime类 |
毋庸置疑此段代码可以正常弹出计算器,运用的原理是Java
库中存在一个共有类java.lang.Runtime类,这个实例存在于每个Java
应用中,它允许应用程序与允许程序的环境交互。当前运行可以从getRuntime
方法获得;但应用程序无法创建自己的此类实例。
因此我们如果想执行命令,需要先使用该类的主要方法getRUntime();它可以让我们获得与当前程序相关的Runtime类的对象;其中Runtime类可以调用**exec()方法执行命令。因此我们执行命令的形式是Runtime.getRuntime().exec(“calc.exe”);,先用Runtime.getRuntime()获取类的对象,然后我们就可以调用其中的实例方法比如exec(command)**了,然后输入参数就可执行各种命令了。
一些其他引用反射的方式
我们上面说到可以通过forName拿到一个类,并且利用反射或者实例化来调用其中的方法,但是如果一个类没有午餐构造方法或者类似单例模式里面的静态方法,那么我们应该怎样通过反射实例化该类呢?
如果一个方法或者构造方法是私有方式,我们应该如何去执行它呢
换句话就是上文提及的**newinstance()和getMethod().invoke()**都无法使用了。
对于这个问题我们可以引入一个新的反射方法 **getConstructor(),它可以根据参数类型(可变参数)来获取公共的构造器Constructor[](public)**。
指定的构造方法生成类的实例
getConsturctor()
当一个类没有getRuntime这样的获取实例的方法且灭有公共的无参构造方法时,就要用到getConsturctor()
函数
1 | getConstructor(Class...) //获取某个public的Constructor |
选定后我们可以通过newInstance(),并传入构造函数的参数执行构造函数,即newInstance(传入的构造函数参数)。
与getMethod
相似,getConstructor接收的参数是构造函数的列表类型,因为构造函数也支持重载,所以需要使用参数列表类型才能唯一确定一个构造函数。
ProcessBuilder
ProcessBuilder用于创建操作系统进程,它提供一个启动和管理金星(也就是应用程序)的方法,我们可以通过实例化这个类并且通过反射调用其中的start
方法来启动一个子进程。当getRuntime被禁用是我们可以通过ProBuilder来执行命令。
1 | public ProcessBuilder(List<String> command) |
ProcessBuilder
有两个构造函数:public ProcessBuilder(List<String> command)
与public ProcessBuilder(String… command)
,我们常用第一种构造函数进行构造,和 getMethod
类似, getConstructor
接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数,当我们获取到构造函数后,我们使用 newInstance
来执行。我们直接向getConstructor
传入List.class
,此时我们可以构造出第一个poc:
1 | import java.util.Arrays; |
此时我们来分析一下执行过程
执行过程
1.利用反射获取ProcessBuilder类
首先我们先使用反射来获取到ProcessBuilder类
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
2.获取形参列表的构造函数
此时我们获取clazz(ProcessBuilder)形参列表为List
1 | (ProcessBuilder)clazz.getConstructor(List.class) |
3.对获取到的方法利用newInstance
进行实例化,调用构造函数
此时我们使用newInstance()对我们获取到的start方法进行实例化
4.对于构造函数传入参数,并将要执行的命令转为List
类型
此时我们对构造函数传入参数calc.exe,并且使用Arrays.asList方法将要执行的命令转为List类型
1 | newInstance(Arrays.asList("calc.exe"))).start(); |
5.返回List
类型的command
利用反射完善payload
但是我们此时使用到了Java里面的强制类型转换,有时候我们利用漏洞的时候可能没法直接使用,所以还是需要反射来完成这一步
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
当我们通过getMethod(“start”)获取到start方法然后invoke执行,这个时候invoke的第一个参数是ProcessBuilder Object;此时我们若是想使用public ProcessBuilder(String...command)
这个构造函数时;我们想获取目标函数里可包含可变长参数,我们可以直接把它认为是数组。这也意味着我们可以将字符串数组的类String[].class传给getConstuctor获取ProcessBuilder的第二种构造函数
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
在调用newInstance的时候,因为这个函数本身接收的是一个可变长参数,我们传给ProcessBuilder的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
执行私有方法
getDeclaredMethod()
现在看向第二个问题,如果一个方法或构造方法是私有方法,我们是否能执行它呢?这就涉及到 getDeclared系列的反射了,与普通的getMethod 、getConstructor
有所不同,getMethod系列方法获取的是当前类中所有公共方法,包括从父类继承的方法;而getDeclaredMethod系列方法获取的是当前类中声明的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了,也就是包含一个类中所有的共有方法。
前文中我们提到由于单例模式,Runtime
这个类的构造函数是私有的,我们需要用Runtime.getRuntime()
来获取对象,然后才能执行命令。但其实现在我们也可以直接用getDeclaredConstructor来获取这个私有的构造方法来实例化对象,进而执行命令:
1 | Class clazz = Class.forName("java.lang.Runtime"); |
在获取到私有方法后,通过setAccessible(true)可以打破私有方法访问限制,从而进行调用。在其他地方的使用,getDeclaredMethod、getDeclaredConstructor和getMethod、getConstructor使用方法是一致的