反射是大多数语言里里都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语⾔言附加上动态特性。
java反射 举一个简单的动态代理的例子
如果使用静态代理,代理类和被代理类在编译期间就确定下来了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 interface ClothFactory { void produceCloth () ; } class ProxyClothFactory implements ClothFactory { private ClothFactory factory; public ProxyClothFactory (ClothFactory factory) { this .factory = factory; } @Override public void produceCloth () { System.out.println("代理工厂正在准备" ); factory.produceCloth(); System.out.println("代理工厂做一些后续的收尾工作" ); } } class NikeClothFactory implements ClothFactory { @Override public void produceCloth () { System.out.println("NIKE工厂生产中" ); } } public class StaticProxyTest { public static void main (String[] args) { NikeClothFactory nike = new NikeClothFactory(); ProxyClothFactory proxyClothFactory = new ProxyClothFactory(nike); proxyClothFactory.produceCloth(); } }
输出结果如下
而使用动态代理则需要在运行的时候才能确定代理类和被代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;interface Human { String getBelief () ; void eat (String food) ; } class SuperMan implements Human { @Override public String getBelief () { return "I believe I can fly!" ; } @Override public void eat (String food) { System.out.println("我喜欢吃" + food); } } class HumanUtil { public void method1 () { System.out.println("====================通用方法一====================" ); } public void method2 () { System.out.println("====================通用方法二====================" ); } } class ProxyFactory { public static Object getProxyInstance (Object obj) { MyInvocationHandler handler = new MyInvocationHandler(); handler.bind(obj); return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),handler); } } class MyInvocationHandler implements InvocationHandler { private Object obj; public void bind (Object obj) { this .obj = obj; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { HumanUtil util = new HumanUtil(); util.method1(); Object returnValue = method.invoke(obj,args); util.method2(); return returnValue; } } public class ProxyTest { public static void main (String[] args) { SuperMan superMan = new SuperMan(); Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan); String belief = proxyInstance.getBelief(); System.out.println(belief); proxyInstance.eat("四川麻辣烫" ); System.out.println("*****************************" ); NikeClothFactory nikeClothFactory = new NikeClothFactory(); ClothFactory proxyClothFactory = (ClothFactory) ProxyFactory.getProxyInstance(nikeClothFactory); proxyClothFactory.produceCloth(); } }
输出结果如下
在反射中最重要的几个方法无非:
获取类的方法: forName
实例化类对象的方法: newInstance
获取函数的方法: getMethod
执行函数的方法: invoke
在反射中我们一般有四种方法来获取类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Class<PersonTest> aClass = PersonTest.class; System.out.println(aClass); PersonTest p = new PersonTest(); Class<? extends PersonTest> aClass1 = p.getClass(); System.out.println(aClass1); Class<?> aClass2 = Class.forName("PersonTest" ); System.out.println(aClass2); ClassLoader loader = ClassTest.class.getClassLoader(); Class<?> aClass3 = loader.loadClass("PersonTest" ); System.out.println(aClass3);
我们一般最经常使用的获取类方法是第三种:调用Class的静态方法:forName(String classPath)
forName有两个函数重载,其中第一种方式可以理解为第二种方式的封装
Class<?> forName(String name)
Class<?> forName(String name, boolean initialize, ClassLoader loader)
默认情况下, forName
的第一个参数是类名;第二个参数表示是否初始化;第三个参数就是 ClassLoader
。
展开说一下ClassLoader
,我们首先看一下类的加载过程
而其中类的加载器的作用就是把类装载进内存,JVM规范了几种扩展类的加载器,分别为引导类加载器、扩展类加载器以及系统类加载器
引导类加载器:用C++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库。该加载器无法直接获取
扩展类加载器:负责jre/liblext目录下的jar包或-java.ext.dirs指定目录下的jar包装入工作库
系统类加载器:负责java -classpath或-Djava.class.path所指的目录下的类与jar包装入工作,是最常用的加载器
那么我们这里可以对这几种加载器进行一下测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void test3 () { ClassLoader loader = ClassTest.class.getClassLoader(); System.out.println(loader); ClassLoader loader1 = loader.getParent(); System.out.println(loader1); ClassLoader loader2 = loader1.getParent(); System.out.println(loader2); }
如下所示,引导类加载器是不能够获取的
继续回到上面,第二个参数 initialize
常常被人误解,使用功能”.class”来创建Class对象的引用时,不会自动初始化该Class对象,使⽤用forName()会自动初始化该Class对象
其实在 forName
的时候,构造函数并不会执行,即使我们设置 initialize=true
。可以将这个“初始化”理理解为类的初始化。
这里又要说到初始化方法的调用过程了,借用p牛代码
1 2 3 4 5 6 7 8 9 10 11 public class TrainPrint { { System.out.printf("Empty block initial %s\n" , this .getClass()); } static { System.out.printf("Static initial %s\n" , TrainPrint.class);q } public TrainPrint () { System.out.printf("Initial %s\n" , this .getClass()); } }
在上面的三种初始化方法中,static
方法是会最先被调用的,因为static
方法在类进行初始化之前就已经调用,而 {} 中的代码会放在构造函数的 super() 后面,但在当前构造函数内容的前面。所以说, forName
中的 initialize=true
其实就是告诉Java虚拟机是否执行’’类初始化’’
newInstance()
:调用此方法,创建对应的运行时类的对象,内部调用了运行时类的空参的构造器
要想此方法正常的创建运行时类的对象,要求:
1.运行时类必须提供空参的构造器 2.空参的构造器的访问权限得够。通常,设置为public
在javabean中要求提供一个public的空参构造器。原因:
1.便于通过反射,创建运行时类的对象 2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器
那么我们这里写个测试,这里是有空参的构造器且权限可以访问的情况,是能够创建对象的
再看一下newInstance
不成功的情况,这里调用Runtime
会报错java.lang.IllegalAccessException
,就是因为他的构造方法为private
1 2 3 4 5 6 7 8 public class RETest1 { public static void main (String[] args) throws Exception { Class clazz = Class.forName("java.lang.Runtime" ); Object invoke = clazz.getMethod("exec" , String.class).invoke(clazz.newInstance(), "id" ); System.out.println(invoke); } }
这里的Runtime
构造方法为私有,是因为Runtime
的设计模式为单例模式,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取。这样,只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance
获取这个对象,避免建立多个数据库连接。
1 2 3 4 5 6 7 8 9 public class TrainDB { private static TrainDB instance = new TrainDB(); public static TrainDB getInstance () { return instance; } private TrainDB () { } }
Runtime
类就是单例模式,我们只能通过 Runtime.getRuntime()
来获取到 Runtime
对象,修改后即可
1 2 3 4 5 6 public static void main (String[] args) throws Exception { Class clazz = Class.forName("java.lang.Runtime" ); clazz.getMethod("exec" , String.class).invoke(clazz.getMethod("getRuntime" ).invoke(clazz), "calc.exe" ); }
getMethod
的作用是通过反射获取一个类的某个特定的公有方法,Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod
的时候,我们需要传给他你需要获取的函数的参数类型列表。
invoke
的作用是执行方法,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数是类
这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]...)
,其实在反射里就是 method.invoke([1], [2], [3], [4]...)
所以我们将上述命令执行的Payload分解一下就是:
1 2 3 4 5 Class clazz = Class.forName("java.lang.Runtime" ); Method execMethod = clazz.getMethod("exec" , String.class); Method getRuntimeMethod = clazz.getMethod("getRuntime" ); Object runtime = getRuntimeMethod.invoke(clazz); execMethod.invoke(runtime, "calc.exe" );
这里就会衍生出两个问题
如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢? 如果一个方法或构造方法是私有方法,我们是否能执行它呢?
那么这时候我们就需要获取可使用的构造器,使用到getConstructors
getConstructors()∶获取当前运行时类中声明为public的构造器
getDecLaredConstructors():获取当前运行时类中声明的所有的构造器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void test1 () { Class<PersonTest2> aClass = PersonTest2.class; Constructor<?>[] constructors = aClass.getConstructors(); for (Constructor c : constructors) { System.out.println(c); } System.out.println(); Constructor<?>[] constructors1 = aClass.getDeclaredConstructors(); for (Constructor c : constructors1) { System.out.println(c); } }
那么我们可以先使用getConstructor
获取函数之后再调用newInstance
1 2 Class clazz = Class.forName("java.lang.ProcessBuilder" ); ((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe" ))).start();
ProcessBuilder有两个构造函数:
public ProcessBuilder(List command)
public ProcessBuilder(String… command)
上面用到了第一个形式的构造函数,在 getConstructor
的时候传入的是 List.class
。
但是,我们看到,前面这个用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。
1 2 Class clazz = Class.forName("java.lang.ProcessBuilder" ); clazz.getMethod("start" ).invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe" )));
通过 getMethod("start")
获取到start
方法,然后 invoke
执行, invoke
的第一个参数就是ProcessBuilder Object
了
那么如果没有public
权限的函数,我们是不是就不能进行反射了呢,也不是,可以通过getDeclaredField
方法来进行操作
调用运行时类的指定结构的过程中,必须要声明属性为public
,那么我们看一下使用常规方法是否会报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void test4 () throws Exception { Class<PersonTest2> aClass = PersonTest2.class; PersonTest2 p = aClass.newInstance(); Field id = aClass.getField("id" ); Field id = aClass.getField("age" ); id.set(p,10 ); System.out.println(id.get(p)); }
我们再试一下getDeclaredField
方法,这里需要用到setAccessible(true)
保证当前属性是可访问的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void test5 () throws Exception { Class<PersonTest2> aClass = PersonTest2.class; PersonTest2 p = aClass.newInstance(); Field name = aClass.getDeclaredField("name" ); name.setAccessible(true ); name.set(p, "neymar" ); System.out.println(name.get(p)); }
再使用下getDeclaredField
进行命令执行
1 2 3 4 Class clazz = Class.forName("java.lang.Runtime" ); Constructor m = clazz.getDeclaredConstructor(); m.setAccessible(true ); clazz.getMethod("exec" , String.class).invoke(m.newInstance(), "calc.exe" );