反射机制
反射概念
在Java中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建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
| interface fruit{ public abstract void eat(); }
class Apple implements fruit{ public void eat(){ System.out.println("Apple"); } }
class Orange implements fruit{ public void eat(){ System.out.println("Orange"); } }
class Factory{ public static fruit getInstance(String fruitName){ fruit f=null; if("Apple".equals(fruitName)){ f=new Apple(); } if("Orange".equals(fruitName)){ f=new Orange(); } return f; } } class hello{ public static void main(String[] a){ fruit f=Factory.getInstance("Orange"); f.eat(); }
}
|
如果我们想要添加新的水果,就需要
- 添加新的水果类
- 修改Factory
- 在main函数中使用新的水果类
修改为反射机制的代码:
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
| interface fruit{ public abstract void eat(); }
class Apple implements fruit{ public void eat(){ System.out.println("Apple"); } }
class Orange implements fruit{ public void eat(){ System.out.println("Orange"); } }
class Factory{ public static fruit getInstance(String ClassName){ fruit f=null; try{ f=(fruit)Class.forName(ClassName).newInstance(); }catch (Exception e) { e.printStackTrace(); } return f; } } class hello{ public static void main(String[] a){ fruit f=Factory.getInstance("Reflect.Apple"); if(f!=null){ f.eat(); } } }
|
这时候如果我们需要添加水果,只需要
那么好像可以看出一点问题,如果传入的类名可控,再加上一些办法,那就不是可以调用任意类,去运行系统命令了呢。或者执行危险的命令
反射组成相关的类
反射机制相关操作一般位于java.lang.reflect包中。
而java反射机制组成需要重点注意以下的类:
java.lang.Class:类对象;
java.lang.reflect.Constructor:类的构造器对象;
java.lang.reflect.Field:类的属性对象;
java.lang.reflect.Method:类的方法对象;
反射常见使用的方法
获取类的方法:forname
实例化类对象的方法:newInstance
获取函数的方法:getMethod
执行函数的方法:invoke
class对象的获取方法
java反射机制的原理基础是理解Class类,在反射中,我们想获取一个类或调用一个类的方法,需要先获取到该类的Class对象。
对于普通用户我们可以采用以下方法创建实例:
1
| Person test = new Person();
|
而我们在创建class类的实例对象却不能使用上述方法,运行会抛出错误
1
| Class test = new Class()
|
同时我们可以跟进Class类的源码进行查看,发现其构造器是私有的,所以只有JVM能够创建Class对象。
因为Class类是private私有属性,我们也无法通过创建对象的方式来获取class对象,那么我们怎样才能够获取到class对象呢?一般我们获取class对象就有以下三种方法,我们来逐一看看。
1、类的.class属性
第一种就是最简单明了的方式,我们可以通过类名的属性class获取。
1
| Class c1=ReflectDemo.class;
|
ReflectDemo是一个已经加载的类,想要获取它的java.lang.Class对象,直接拿取class参数即可。(这不是反射机制)
2、实例化对象的getClass()方法
第二种我们可以先实例化一个对象,之后在调用getClass()方法。
1 2
| ReflectDemo demo2= new ReflectDemo(); Class c2 = demo2.getClass();
|
- 假如obj是实例:获取该实例的class(如Runtime.getRuntime().getClass()结果就是class java.lang.Runtime类)(此处类的意思实际上时class这个类的对象)
- 假如obj是类:获取到java.lang.Class类(class这个类的对象)
3、Class.forName(String className):动态加载类
第三种则是调用Class类中的forName方法,将字节码文件加载进内存,返回Class对象。
如果知道类的名字,可以直接使用forname来获取。
forName两种使用形式
1 2 3 4 5 6 7 8 9 10
| Class<?> forName(String name)
Class<?> forName(String name, **boolean** initialize, ClassLoader loader)
Class.forName(className) Class.forName(className, true, currentLoader)
|
类初始化:
类初始化不等于类的实例化,举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Demo1 { { System.out.println("初始化块:"+this.getClass()); } static { System.out.println("静态初始块:"+Demo1.class); } public Demo1(){ System.out.println("构造函数:"+this.getClass()); } public static void main(String[] args) throws ClassNotFoundException { Class.forName("com.test.Demo1"); } }
|
类的实例化:静态初始块
->初始块
->构造函数
类的初始化:静态初始块
具有父类的类的实例化:父类静态初始块
->子类静态初始块
->父类初始块
->父类构造函数
->子类初始块
->子类构造函数
具有父类的类的初始化:父类静态初始块
->子类静态初始块
写个简单的示例代码,分别利用这三种方法获取当前类Class对象的当前类名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Demo2 { public static void main(String[] args) throws ClassNotFoundException { Class<?> c1 = Demo2.class; System.out.println(c1);
Demo2 demo2 = new Demo2(); Class<?> c2 = demo2.getClass(); System.out.println(c2);
Class<?> c3 = Class.forName("com.test.Demo2"); System.out.println(c3);
} }
|
在这三种获取CLass类方式中,我们一般使用第三种通过Class.forName方法去动态加载类。且使用forName就不需要import导入其他类,可以加载我们任意的类。
而使用类.class属性,需要导入类的包,依赖性太强,在大型项目中容易抛出编译错误;
而使用实例化对象的getClass()方法,需要本身创建一个对象,本身就没有了使用反射机制意义。
所以我们在获取class对象中,一般使用Class.forName方法去获取。
获取成员变量Field
获取成员变量Field位于java.lang.reflect.Field包中
Field[] getFields() :获取所有public修饰的成员变量
Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
Field getField(String name) 获取指定名称的 public修饰的成员变量
Field getDeclaredField(String name) 获取指定的成员变量
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
| import java.lang.reflect.Field;
public class FieldTest { public String name; public String profession; protected int age; private String number; char sex;
public static void main(String[] args){ try{
Class c1 = Class.forName("com.test.FieldTest");
Field[] fieldArray1 = c1.getDeclaredFields(); Field[] fieldArray2 = c1.getFields();
for (Field field : fieldArray1){ System.out.println(field.getName()); }
System.out.println("-------分割线---------");
for (Field field : fieldArray2){ System.out.println(field.getName()); } System.out.println("-------分割线---------");
Field fieldArray3 = c1.getField("name"); System.out.println(fieldArray3.getName()); System.out.println("-------分割线---------");
Field fieldArray4 = c1.getDeclaredField("number"); System.out.println(fieldArray4.getName());
} catch (Exception e) { e.printStackTrace(); } } }
|
获取成员方法Method
Method getMethod(String name, 类<?>… parameterTypes) //返回该类所声明的public方法
Method getDeclaredMethod(String name, 类<?>… parameterTypes) //返回该类所声明的所有方法
//第一个参数获取该方法的名字,第二个参数获取标识该方法的参数类型
Method[] getMethods() //获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法
Method[] getDeclaredMethods() // 获取该类中的所有方法
我们使用getMethod作用通过反射获取一个类的某个特定的公有方法。且java中支持类的重载,我们不能仅通过函数名确定一个函数。在调用getMethod时候,需要传给他你需要获取的函数的参数类型列表。
如Runtime.exec方法有6个重载:
Class.forName(“java.lang.Runtime”).getMethod(“exec”, String.class)
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
| package com.test;
import java.lang.reflect.Method;
public class MethodTest { public void study(String s) { System.out.println("学习中..." + s); }
protected void run() { System.out.println("跑步中..."); }
void eat() { System.out.println("吃饭中..."); }
private String sleep(int age) { System.out.println("睡眠中..." + age); return "sleep"; }
public static void main(String[] args) { try { Class c = Class.forName("com.test.MethodTest"); // 创建Class对象 Method[] methods1 = c.getDeclaredMethods(); // 获取所有该类中的所有方法 Method[] methods2 = c.getMethods(); // 获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法
for (Method m:methods1) { System.out.println(m); }
System.out.println("-------分割线---------");
for (Method m:methods2) { System.out.println(m); }
System.out.println("-------分割线---------");
Method methods3 = c.getMethod("study", String.class); // 获取study方法 System.out.println(methods3); System.out.println("-------分割线---------");
Method method4 = c.getDeclaredMethod("sleep", int.class); // 获取sleep方法 System.out.println(method4);
} catch (Exception e) { e.printStackTrace(); } } }
|
获取构造函数Constructor
Constructor<?>[] getConstructors() :只返回public构造函数
Constructor<?>[] getDeclaredConstructors() :返回所有构造函数
Constructor<> getConstructor(类<?>… parameterTypes) : 匹配和参数配型相符的public构造函数
Constructor<> getDeclaredConstructor(类<?>… parameterTypes) : 匹配和参数配型相符的构造函数
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
| package com.test;
import java.lang.reflect.Constructor;
public class ConstructorTest { public ConstructorTest() { System.out.println("无参构造函数"); } public ConstructorTest(String name) { System.out.println("有参构造函数" + name); } private ConstructorTest(boolean n) { System.out.println("私有构造函数"); } public static void main(String[] args) { try { Class c1 = Class.forName("com.test.ConstructorTest"); Constructor[] constructors1 = c1.getDeclaredConstructors(); Constructor[] constructors2 = c1.getConstructors(); for (Constructor c : constructors1) { System.out.println(c); } System.out.println("-------分割线---------"); for (Constructor c : constructors2) { System.out.println(c); } System.out.println("-------分割线---------"); Constructor constructors3 = c1.getConstructor(String.class); System.out.println(constructors3); System.out.println("-------分割线---------"); Constructor constructors4 = c1.getDeclaredConstructor(boolean.class); System.out.println(constructors4); } catch (Exception e) { e.printStackTrace(); } } }
|
invoke
invoke方法位于Method类下,其的作用是传入参数,执行方法,
public Object invoke(Object obj, Object... args)
它的第一个参数是执行method的对象:
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数是类(之后会提到,这里其实不用那么死板,这个)
它接下来的参数才是需要传入的参数。
由于我们的exec函数是一个普通方法,需要传入类对象,即invoke(类对象,exec方法传入的参数)
之前说到Runtime的类对象不能通过newInstance()来获取对象(class.newInstance等于new class),是因为Runtime的类构造函数是一个private构造函数,只能通过getRuntime方法返回一个对象。
获取类对象:Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))
(由于getRuntime是一个静态方法,invoke传入Runtime类,进行调用)
invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),"calc.exe")
合成以上的操作:
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 2
| Class clazz = Class.forName("java.lang.Runtime"); clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
|
这种方式虽然弹出了计算器,但仅仅只是调用其方法去实现。而我们还是想执行其私有方法,通过newInstance来构造使用。这样就需要我们通过setAccessible(true)来突破访问权限的检查。
设置setAccessible(true)暴力访问权限
在一般情况下,我们使用反射机制不能对类的私有private字段进行操作,绕过私有权限的访问。但一些特殊场景存在例外的时候,比如我们进行序列化操作的时候,需要去访问这些受限的私有字段,这时我们可以通过调用AccessibleObject上的setAccessible()方法来允许访问。
还需要用到getDeclaredConstructor
这个前面介绍了 可以获取到的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了
可以看到是私有的构造函数
不能直接newInstance来实例化类
代码如下:一样可以弹计算器
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
| package com.test;
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException;
public class RuntimeTest { public static void main(String[] args) { try { Class<?> aClass = Class.forName("java.lang.Runtime"); Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(); declaredConstructor.setAccessible(true); aClass.getMethod("exec",String.class).invoke(declaredConstructor.newInstance(),"calc.exe"); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } } }
|
ProcessBuilder
假如一个类没有无参构造方法(即不能class.newInstance()),也没有单例模式(只存在一个实例)的静态方法(即不能像getRuntime一样获取实例),那我们该如何实例化这个类呢?
这个时候可以用ProcessBuilder 通过反射来获取其构造函数,然后调用start()来执行命令
1 2 3 4
| List<String> paramList = new ArrayList<>(); paramList.add("calc.exe"); ProcessBuilder pb = new ProcessBuilder(paramList); pb.start();
|
其构造函数是写入了一个字符串,不是无参构造方法,接下来我们会一步步进行转化。
1
| getConsturctor()`函数可以选定指定接口格式的构造函数(由于构造函数也可以根据参数来进行重载),即:`getConsturctor(参数类型)
|
选定后我们可以通过newInstance(),并传入构造函数的参数执行构造函数,即newInstance(传入的构造函数参数)
。
ProcessBuilder有两个构造函数:
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)
(此处,String...
这种语法表示String参数数量是可变的,与String[]一样)
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
| package com.test;
import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.List;
public class ProcessBuilderTest1 { public static void main(String[] args) throws IOException { try { Class<?> aClass = Class.forName("java.lang.ProcessBuilder"); Constructor<?> constructor = aClass.getConstructor(List.class); ((ProcessBuilder) constructor.newInstance(Arrays.asList("calc.exe"))).start(); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); }
} }
|
我上面用到了第一个形式的构造函数,所以我在 getConstructor 的时候传入的是 List.class 。
但是,我们看到,前面这个Payload用到了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
| package com.test;
import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.List;
public class ProcessBuilderTest1 { public static void main(String[] args) throws IOException { try { Class<?> aClass = Class.forName("java.lang.ProcessBuilder"); Constructor<?> constructor = aClass.getConstructor(List.class); aClass.getMethod("start").invoke(constructor.newInstance(Arrays.asList("calc.exe"))); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); }
} }
|
那么,如果我们要使用 public ProcessBuilder(String… command) 这个构造函数,需要怎样用反 射执行呢?
这又涉及到Java里的可变长参数(varargs)了。正如其他语言一样,Java也支持可变长参数,就是当你 定义函数的时候不确定参数数量的时候,可以使用 … 这样的语法来表示“这个函数的参数个数是可变 的”。
对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价 的(也就不能重载):
1 2 3
| public void hello(String[] names) {}
public void hello(String...names) {}
|
如果我们有一个数组,想传给hello函数,只需直接传即可:
1 2 3
| String[] names = {"hello", "world"};
hello(names);
|
对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。 所以,我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二 种构造函数:
1 2 3
| Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)
|
在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:
1
| Class clazz = Class.forName("java.lang.ProcessBuilder"); ((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();
|
这种形式也是类型转换
改为反射
1 2 3
| Class<?> aClass = Class.forName("java.lang.ProcessBuilder"); Constructor<?> constructor = aClass.getConstructor(String[].class); aClass.getMethod("start").invoke(constructor.newInstance(new String[][]{{"calc.exe"}}));
|