在写完上个文章JAVA安全基础(一)--类加载器(ClassLoader)之后,打算更新一个java安全基础系列文章,一部分自己学到的东西能够做个知识沉淀,另一部分是想帮助一部分想入门java安全的小伙伴。所以文章总体比较通俗简单,自己的讲述分析和概念应用可能会占比较大的篇幅,比较注重带来一些基础性东西,大佬勿喷~。
反射是Java的特征之一,是一种间接操作目标对象的机制,核心是JVM在运行状态的时候才动态加载类,对于任意一个类都能够知道这个类所有的属性和方法,并且对于任意一个对象,都能够调用它的方法/访问属性。这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。
静态编译:在编译时确定好类型,绑定对象。
动态编译:在运行时确定类型,绑定对象。
反射:将类的各个组成部分封装为其他对象,这就是反射机制。
1、可以在程序运行过程中,操作这些对象。
2、可以解耦,提高程序的可扩展性。
反射机制可以动态创建对象和编译,体现很大的灵活性。在一个大型系统发布时,如果以后需要更新某些功能的时候,不可能让用户将之前的游戏卸载掉,重新安装新的版本。采取静态编译的话,需要把整个游戏重新编译一次才能实现功能的更新,而使用反射机制的话,不需要卸载,只需要在运行的时候动态创建和编译即可。
这里我们使用一个简陋的流程图配合讲述下反射机制基本流程。比如我们创建了一个类文件,经过javac编译之后,就会形成class文件,同时jvm内存会查找生成的class文件读入内存和经过ClassLoader加载,同时会自动创建生成一个Class对象,里面拥有其获取成员变量Field,成员方法Method和构造方法Constructor等方法。最后就是我们平时new创建对象。
这里其实跟我们了解到java运行体系没有很大区别,只是我们生成class文件的时候,也创建了Class对象。而Class是java.lang.Class类中,是所有类的的类。而类是java.lang.Class类的实例对象,所以我们的反射机制本身不复杂,就是获取一个类的Class对象,然后在用Class对象中的获取成员变量Field,成员方法Method和构造方法Constructor等方法,再去动态获取一个类或者调用一个类的属性,变量,构造方法等方式。
反射机制相关操作一般位于java.lang.reflect包中。
而java反射机制组成需要重点注意以下的类:
java.lang.Class:类对象;
java.lang.reflect.Constructor:类的构造器对象;
java.lang.reflect.Field:类的属性对象;
java.lang.reflect.Method:类的方法对象;
获取类的方法:forname
实例化类对象的方法:newInstance
获取函数的方法:getMethod
执行函数的方法:invoke
从上述流程图分析中可以得知,java反射机制的原理基础是理解Class类,在反射中,我们想获取一个类或调用一个类的方法,需要先获取到该类的Class对象。
对于普通用户我们可以采用以下方法创建实例:
Person test = new Person();
而我们在创建class类的实例对象却不能使用上述方法,运行会抛出错误
Class test = new Class();
同时我们可以跟进Class类的源码进行查看,发现其构造器是私有的,所以只有JVM能够创建Class对象。
因为Class类是private私有属性,我们也无法通过创建对象的方式来获取class对象,那么我们怎样才能够获取到class对象呢?一般我们获取class对象就有以下三种方法,我们来逐一看看。
第一种就是最简单明了的方式,我们可以通过类名的属性class获取。
Class c1=ReflectDemo.class;
第二种我们可以先实例化一个对象,之后在调用getClass()方法。
ReflectDemo demo2= new ReflectDemo(); Class c2 = demo2.getClass();
第三种则是调用Class类中的forName方法,将字节码文件加载进内存,返回Class对象。
Class c3 = Class.forName("reflectdemo.ReflectDemo");
我们可以写个简单的示例代码,分别利用这三种方法获取当前类Class对象的当前类名。
public class ReflectTemo { public static void main(String[] args) throws ClassNotFoundException { // 类的.class属性 Class c1 = ReflectTemo.class; System.out.println(c1.getName()); // 实例化对象的getClass()方法 ReflectTemo demo = new ReflectTemo(); Class c2 = demo.getClass(); System.out.println(c2.getName()); // Class.forName(String className): 动态加载类 Class c3 = Class.forName("com.reflect.ReflectTemo"); System.out.println(c3.getName()); } }
但在这三种获取CLass类方式中,我们一般使用第三种通过Class.forName方法去动态加载类。且使用forName就不需要import导入其他类,可以加载我们任意的类。
而使用类.class属性,需要导入类的包,依赖性太强,在大型项目中容易抛出编译错误;
而使用实例化对象的getClass()方法,需要本身创建一个对象,本身就没有了使用反射机制意义。
所以我们在获取class对象中,一般使用Class.forName方法去获取。
获取成员变量Field位于java.lang.reflect.Field包中
Field[] getFields() :获取所有public修饰的成员变量
Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
Field getField(String name) 获取指定名称的 public修饰的成员变量
Field getDeclaredField(String name) 获取指定的成员变量
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.reflect.FieldTest"); // 创建Class对象 Field[] fieldArray1 = c1.getDeclaredFields(); //获取全部成员变量 Field[] fieldArray2 = c1.getFields();// 获取全部public成员变量 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"); // 获取指定名称的public修饰的成员变量 System.out.println(fieldArray3.getName()); System.out.println("-------分割线---------"); Field fieldArray4 = c1.getDeclaredField("number"); // 获取指定的成员变量 System.out.println(fieldArray4.getName()); } catch (Exception e) { e.printStackTrace(); } } }
Method getMethod(String name, 类<?>... parameterTypes) //返回该类所声明的public方法
Method getDeclaredMethod(String name, 类<?>... parameterTypes) //返回该类所声明的所有方法
//第一个参数获取该方法的名字,第二个参数获取标识该方法的参数类型
Method[] getMethods() //获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法
Method[] getDeclaredMethods() // 获取该类中的所有方法
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.reflect.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<?>[] getConstructors() :只返回public构造函数
Constructor<?>[] getDeclaredConstructors() :返回所有构造函数
Constructor<> getConstructor(类<?>... parameterTypes) : 匹配和参数配型相符的public构造函数
Constructor<> getDeclaredConstructor(类<?>... parameterTypes) : 匹配和参数配型相符的构造函数
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.reflect.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(); } } }
在前面我们获取了Class对象,之后展示了一系列获取成员变量、成员方法和成员函数的方式后,我们现在可以通过反射来生成实例化对象,一般我们使用Class对象的newInstance()方法来进行创建类对象。
使用的方式也特别简单,只需要通过forname方法获取到的class对象中进行newInstance方法创建即可。
Class c = Class.forName("com.reflect.MethodTest"); // 创建Class对象 Object m1 = c.newInstance(); // 创建类对象
这里也顺便说下invoke方法,invoke方法位于java.lang.reflect.Method类中,用于执行某个的对象的目标方法。
一般会和getMethod方法配合进行调用。
使用用法
public Object invoke(Object obj, Object... args)
第一个参数为类的实例,第二个参数为相应函数中的参数
obj:从中调用底层方法的对象,必须是实例化对象
args: 用于方法的调用,是一个object的数组,参数有可能是多个
将我们的知识进行整合归纳下,我们可以写个完整的小例子。
import java.lang.reflect.Method; public class ReflectTest { public void reflectMethod() { System.out.println("反射测试成功!!!"); } public static void main(String[] args) { try { Class c = Class.forName("com.reflect.ReflectTest"); // 创建Class对象 Object m = c.newInstance(); // 创建类实例对象 Method method = c.getMethod("reflectMethod"); // 获取reflectMethod方法 method.invoke(m); // 调用类实例对象方法 } catch (Exception e) { e.printStackTrace(); } } }
上面我们讲述了反射机制流程概念和基本使用方法,从Class对象获取,到获取成员变量、成员方法和构造函数,接着是newInstance创建类对象和invoke方法,最后是一个简单反射例子的组成。相信大家都对反射有个很清晰的了解和认识,现在我们来看看我们平时漏洞测试中,如何使用java反射中如何获取Runtime类来命令执行测试弹出计算器。
这里我们可以先模仿前面的反射例子,尝试去反射执行Runtime类去命令执行。
import java.lang.reflect.Method; public class RuntimeTest { public static void main(String[] args) throws Exception { Class c1 = Class.forName("java.lang.Runtime"); Object m = c1.newInstance(); Method method = c1.getMethod("exec", String.class); method.invoke(m,"/System/Applications/Calculator.app/Contents/MacOS/Calculator"); } }
但我们发现了使用newInstance产生了报错的话,而往往其可能是以下原因未能实现。
1、使用的类没有无参构造函数
2、使用的类构造函数是私有的
之后我们查看其出错原因,java.lang.Runtime with modifiers "private"。Runtime类的构造方法是private权限私有的,是根据“单例模式”进行设计的,我们无法进行直接调用。看来我们上述的反射机制也是有限制的,并不能饶过private权限的检查。
既然我现在不能绕过private权限的检测,那我先不管newIntance创建类对象,先获取Rumtime类的方法先谈个计算器再看看。所以我们需要对代码进行简单修改下。
我们使用getMethod作用通过反射获取一个类的某个特定的公有方法。且java中支持类的重载,我们不能仅通过函数名确定一个函数。在调用getMethod时候,需要传给他你需要获取的函数的参数类型列表。
如Runtime.exec方法有6个重载:
exec(String commmand)
我们可以实用最简单这种只有一个参数且类型是String。实用getMethod("exec",String.class)获取Runtime.exec方法,并且获取getRuntime方法来执行。
然后我们将这段代码进行简略下,就生成以下我们常见的反射payload测试:
Class c1 = Class.forName("java.lang.Runtime"); c1.getMethod("exec", String.class).invoke(c1.getMethod("getRuntime").invoke(c1),"/System/Applications/Calculator.app/Contents/MacOS/Calculator");
这里我们虽然成功执行方法弹出计算器,但仅仅只是调用其方法去实现。而我们还是想执行其私有方法,通过newInstance来构造使用。这样就需要我们通过setAccessible(true)来突破访问权限的检查。
在一般情况下,我们使用反射机制不能对类的私有private字段进行操作,绕过私有权限的访问。但一些特殊场景存在例外的时候,比如我们进行序列化操作的时候,需要去访问这些受限的私有字段,这时我们可以通过调用AccessibleObject上的setAccessible()方法来允许访问。
Java.lang.reflect.AccessibleObject类是Field,Method和Constructor类对象的基类,可以提供将反射对象标记为使用它抑制摸人Java访问控制检查的功能,同时上述的反射类中的Field,Method和Constructor继承自AccessibleObject。所以我们在这些类方法基础上调用setAccessible()方法,既可对这些私有字段进行操作。
所有我们最开始的代码再修改简略下,就能成功命令执行了。
import java.lang.reflect.Constructor; public class RuntimeTest { public static void main(String[] args) throws Exception { Class c1= Class.forName("java.lang.Runtime"); Constructor m = c1.getDeclaredConstructor(); m.setAccessible(true); c1.getMethod("exec", String.class).invoke(m.newInstance(), "/System/Applications/Calculator.app/Contents/MacOS/Calculator"); } }
最后结束就用一个之前学习java反射的时候学习的demo,如果我们在进行反序列化过程中可控的话,能够加载我们使用反射调用的恶意命令语句的话,则能够成功进行命令执行。
import java.io.*; import java.lang.reflect.Method; public class demo implements Serializable { private Integer age; private String name; public demo() {} public demo(String name,Integer age) {// 构造函数,初始化时执行 this.age = age; this.name = name; } private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{ in.defaultReadObject(); // 调用原始的readObject方法 try{ // 通过反射方法执行命令; Method method = java.lang.Runtime.class.getMethod("exec", String.class); Object result = method.invoke(Runtime.getRuntime(),"calc.exe"); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { demo x = new demo(); operation.ser(x); operation.deser(); } } class operation{ public static void ser(Object obj) { try {// 序列化操作,写数据 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj")); // ObjectOutStream能把Object输出成Byte流 oos.writeObject(obj); // 序列化关键函数 oos.flush(); //缓冲流 oos.close(); //关闭流 } catch (IOException e) { e.printStackTrace(); } } public static void deser() { try {// 反序列化操作,读取数据 File file = new File("object.obj"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); Object x = ois.readObject(); //反序列化的关键函数 System.out.println(x); ois.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } }
在这个实验当中,我们使用的Java反射却是可以访问private的方法和属性,绕过第二级安全机制的方法,原理本质是官兵访问安全检查。
在实际利用场景当中,我们利用利用Java反射机制来绕过一些安全权限机制检查,如获取private权限的方法和属性。本质就是我们绕过访问安全检查。所以通过java反射机制的学习,能够为我们为我们后面的java漏洞分析调试,java漏洞poc测试和服务端模板引擎注入等有着十分重要的意义。因为文章讲的比较通俗简单,主要是面向开始没有涉及一些漏洞分析与源码分析,后面java安全基础系列会慢慢加入这些并详细讲解。其次如果大家对文章有什么问题和觉得需要改进的地方,可以在评论下方留言,大家一起探讨交流下。
https://xz.aliyun.com/t/7029#toc-0