Java反射机制与安全问题

Java反射机制与安全问题

0x00前言

近日,笔者在总结Java反序列化漏洞的过程中发现一个怎么也绕不开的词“Java反射机制“,以前笔者只知道这是一个实现Java”准动态“语言,方便开发人员调试程序的机制而已,没想到“反射”竟成了反序列化漏洞攻击的手段之一,所以在这篇文章中好好的总结一下,反射机制的原理以及存在还哪些安全问题。

反射机制思维导图:
Java反射机制与安全问题_第1张图片

0x01 Java反射机制定义

Java反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

反射机制很重要的一点就是“运行时”,其使得我们可以在程序运行时加载、探索以及使用编译期间完全未知的 .class 文件。用一句话总结就是,反射可以实现在运行时可以知道任意一个类的属性和方法。

0x02 反射机制优点与缺点

刚接触反射的我们可能会有一个疑问,为什么要用反射,直接创建对象他不香吗,这时候就涉及到了Java中动态编译与静态编译的概念,我们简单说一下。

  • 静态编译:在编译时确定好类型,绑定对象。
  • 动态编译:在运行时确定类型,绑定对象。最大限度地发挥了Java的灵活性,体现了多态性,并降低了类之间的耦合性。

优点:
反射机制可以实现动态创建对象和编译,体现出很大的灵活性,特别是在J2EE的开发者他的灵活性就表现得十分明显。例如,在一个大型软件的开发中,当程序被编译后发布,如果以后需要更新某些功能的时候,我们不可能要用户把以前软件的卸载,再重新安装新的版本。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用卸载,只需要在运行时才动态的创建和编译,就可以实现该功能。

缺点:
对性能有影响。反射机制其实是一种解释操作,我们通过告诉JVM,我们希望做什么并且他们组我们的要求。这类操作总是慢于只直接执行相同的操作

0x03 反射机制原理

反射机制的原理基础是理解Class类,类是java.lang.Class类的实例对象,而Class是所有的类的类。对于普通的对象,我们在创建实例的时候通常采用如下方法:

Demo test = new Demo();

那么我们在创建class类的实例对象时是否可以同样用上面的方法创建呢

Class c = new Class();

答案是不行的,所以我们查看一下Class的源码,发现他的构造器是私有的,这意味着只有JVM可以创建Class的对象。
在这里插入图片描述

虽然我们不能像创建一个普通对象一样使用new,实例化一个对象,但是却可以通过随便一个已有的类得到一个Class对象,共有三种方式,如下:(假设存在一个叫Demo的普通类)

  1. 实例化一个普通的其他类,然后对这个实例调用getClass()方式获得Class对象
Demo test = new Demo();
Class c1 = test.getClass();
  1. 任何数据类型(包括基本数据类型)都有一个“静态”的class属性,所以直接调用.class属性获得Class对象
Class c2 = Demo.class;
  1. 调用Class类的forNmae方法,获得Class的对象
Class c3 = Class.forName(“ReflectDemo.Demo”)//forName()

参数是真实路径,包名.类名
Java反射机制与安全问题_第2张图片
在这里插入图片描述

三种创建方式通常用第三种,原因是第一种都已经创建了对象,已经失去了反射机制使用的意义;第二种需要导入类的包,依赖性太强,不导包就抛出编译错误。一般都是用第三种方法,一个字符串可以用传入的方法也可以写在配置文件中等多种方法。

反射机制原理就是把java类中的各种成分映射成一个个的java对象,所以我们可以在运行时调用类中的所有成员(变量、方法)。下图是反射机制中类的加载过程:
Java反射机制与安全问题_第3张图片

0x04 Java反射机制操作

通过前面的介绍,我们了解了怎样获得Class的对象,而且通过反射机制,可以获得Class对象的所有成员信息,那么我们简单介绍一下获取成员的一些函数:

  • 获取成员方法Method
public Method getDeclaredMethod(String name, Class... parameterTypes) 

//得到该类所有的方法,不包括父类的。

public Method getMethod(String name, Class... parameterTypes) 

//得到该类所有的public方法,包括父类的。

两个参数分别是方法名和方法参数类的类类型列表(class type)
假如类A存在四种成员方法,如下:
Java反射机制与安全问题_第4张图片
利用getDeclaredMethod()和getMethod()函数,获取指定class中的所有/public 成员方法
Java反射机制与安全问题_第5张图片
在这里插入图片描述

  • 获取构造函数Constructor
`public Constructor getDeclaredConstructor(Class... parameterTypes)` 

//获得该类所有的构造器,不包括其父类的构造器。

public Constructor getConstructor(Class... parameterTypes) 

//获得该类所有public构造器,包括父类。

假如类A存在三种构造函数,两种public和一种private构造器
Java反射机制与安全问题_第6张图片
利用getDeclaredConstructor()函数,获取全部构造器;利用getConstructor()函数,只获取public构造器。
Java反射机制与安全问题_第7张图片
在这里插入图片描述

  • 获取成员变量Field
public Field getDeclaredField(String name) 

//获得该类自身声明的所有变量,不包括其父类的变量。

public Field getField(String name)

//获得该类自所有的public成员变量,包括其父类变量。

类的成员变量也是一个对象,它是java.lang.reflect.Field的一个对象,所以我们通过java.lang.reflect.Field里面封装的方法来获取这些信息。
A类中存在一些不同属性的成员变量:
Java反射机制与安全问题_第8张图片
利用getDeclaredFields()函数,获取全部成员变量;利用getFields()函数,只获取public成员变量。
Java反射机制与安全问题_第9张图片
在这里插入图片描述

0x05 反射机制与反序列化漏洞

反序列化漏洞的关键函数:
writeObject()序列化,将Object输出成Byte流
readObject()反序列化,将Byte流输出成Object

利用反射机制,重写readObject方法,加入能够进行命令执行的函数Runtime.getRuntime(),执行calc.exe命令调出计算器

package reflectdemo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
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();//调用原始的readOject方法
    	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"));
	        //ObjectOutputStream能把Object输出成Byte流
	        oos.writeObject(obj);//序列化关键函数
	        oos.flush();  //缓冲流 
	        oos.close(); //关闭流
	    } catch (FileNotFoundException e) {        
	        e.printStackTrace();
	    } 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.print(x);
			ois.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

从上面的反序列化漏洞可以看出,Java反射确实可以访问private的方法和属性,这是绕过第二级安全机制的方法(之一)。它其实是Java本身为了某种目的而留下的类似于“后门”的东西,或者说是为了方便调试。不管如何,它的原理其实是关闭访问安全检查。
Java反射机制与安全问题_第10张图片

总的来说,当我们发现漏洞想让程序实现命令执行的时候有两个方向可以努力

  1. 控制代码、函数:就像命名注入等注入类漏洞一样数据被当作了代码执行;或者和上面的Demo代码一样重写readObject,加入自定义的代码
  2. 控制输入、数据、变量:利用代码中已有的函数和逻辑,通过改变输入内容的形态实现流程的控制(不同的输入会走不同的逻辑流程,执行不同的代码块中的代码)

对于Java反序列化漏洞来说,这属于控制数据输入一类。在调用反射机制触发漏洞时,他有两个基本点必须要满足:

  1. 有一个可序列化的类,并且该类是重写了readObject()方法的(由于不存在代码注入,只能查找已有代码逻辑中是否有这样的类)
  2. 在重写的readObject()方法的逻辑中有method.invoke函数出现,而且参数可控。

0x06 反射安全

看了上面的内容,我们应该由衷的感叹,Java反射机制实在是太强大了。但是,如果我们对安全有一定意识的话,就会发现Java这个机制强大的似乎有些过头了。

在处理反射时安全性是一个较复杂的问题。反射经常由框架型代码使用,由于这一点,我们可能希望框架能够全面接入代码,无需考虑常规的接入限制。但是,在其它情况下,不受控制的接入会带来严重的安全性风险。

由于这些互相矛盾的需求,Java编程语言定义一种多级别方法来处理反射的安全性。基本模式是对反射实施与应用于源代码接入相同的限制:

  1. 从任意位置到类公共组件的接入
  2. 类自身外部无任何私有组件接入
  3. 受保护和打包(缺省接入)组件的有限接入

相对于C++来说,Java算是比较安全的语言了。这与它们的运行机制有密切的关系,C++运行于本地,也就是说几乎所有程序的权限理论上都是相同的。而Java由于是运行于虚拟机中,而不直接与外部联系,所以实际上Java的运行环境是一个“沙盒”环境。而且作为Java的安全模型,它包括了:字节码验证器、类加载器、安全管理器、访问控制器等一系列的安全组件,所以显得Java的安全机制比较复杂的。

你可能感兴趣的:(美创安全实验室)