Java 反序列化问题和反射机制 2018-10-30 02:00:29 Steven Xeldax ## JAVA反序列化漏洞原理与应用 很多语言都有反序列化这个功能,反序列化的出现使得我们代码中的变量能够随时保存在长效存储介质中(文件系统或数据库等)然后随时能取出在另外一个代码处使用。虽然这种特性很便利,但是也伴随着很多安全问题。 例如php的serialize()和python的pickle都成为漏洞挖掘者所第一关注的焦点。php的反序列化漏洞存在原因在于其核心魔术方法,那么java的反序列化漏洞是不是呢? 实现一个最基础的java反序列化代码: JavaSerialize.java代码 ``` import java.io.*; public class JavaSerialize { public static void main(String args[]) throws Exception{ MyObject myObj = new MyObject(); myObj.name = "hi"; FileOutputStream fos = new FileOutputStream("object"); ObjectOutputStream os = new ObjectOutputStream(fos); os.writeObject(myObj); os.close(); FileInputStream fis = new FileInputStream("object"); ObjectInputStream ois = new ObjectInputStream(fis); MyObject objectFromDisk = (MyObject)ois.readObject(); System.out.println(objectFromDisk.name); ois.close(); } } ``` MyObject代码 ``` import java.io.*; public class MyObject implements Serializable{ public String name; private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{ in.defaultReadObject(); Runtime.getRuntime().exec("calc.exe"); } } ``` tips:注意,这里必须引入Serializable的接口继承,如果没有Serializable接口就默认不具有序列化特性,不像php和python拿来一个类用函数就可以序列化。如果没有接口会报错 当运行上述代码的时候就会调用序列化代码,执行windows10的计算器。 我们可以发现在上述这段代码中核心关键在于readObject这一个函数。readObject() 方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回,readObject() 是可以重写的,可以定制反序列化的一些行为。 这里我们知道java反序列化漏洞成因是因为,需要被反序列化的类中重写了readObject方法,然而重写的readObject方法中执行了命令。 我相信有些常识的开发者都不会直接将命令写在readObject中,因此此处就需要通过反射链来进行任意代码执行了。 对java序列化的详解: 1、api定位: java.io.ObjectOutputStream -> writeObject() 序列化把对象序列化成字节流 java.io.ObjectInputStream -> readObject() 反序列化读取字节流反序列化对象 2.实现Serializable和Externalizable接口的类才能序列化与反序列化。 3.java的反射机制: a.对于任何一个类,都能判断对象所属的类。 b.对于任何一个类,都能获取其所有的属性和方法。 c.对于任何一个对象,都能调用任意一个方法和属性。 ## java的反射机制 JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。 JAVA反射(放射)机制:“程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言”。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。但是JAVA有着一个非常突出的动态相关机制:Reflection,用在Java身上指的是我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。 tips:最常见的反射 运用static method------Class.forName()(最常被使用) Class c1 = Class.forName ("java.lang.String"); 通常的单单叙述反射的机理是很枯燥很抽象,难以去理解到底什么是java的反射机制。所以这里就使用一个简单的例子来阐述反射。 ``` public class Test{ public static void main(String[] args) throws Exception{ File springConfigFile = new File("classConfig.txt"); Properties springConfig = new Properties(); String className = (String)springConfig.get("class"); String methodName = (String)springConfig.get("method"); //根据类名称创建类对象 Class clazz = Class.forName(className); //根据方面名称,获取方法 Method m = clazz.getMethod(methoName); //获取构造器 Constructor c = clazz.getConstructor(); //根据构造器,实例化出对象 Object service = c.newInstance(); //调用对象的制定方法 m.invoke(service); } } ``` classConfig.txt中的内容: ``` class = reflection.Service1 method = doService1 ``` 在测试类Test中,首先取出类名称和方法名,然后通过反射去调用这个方法。 当需要从调用第一个业务方法,切换到调用第二个业务方法的时候,不需要修改一行代码,也不需要重新编译,只需要修改配置文件spring.txt,再运行即可。 使用这个例子,可以较好得理解反射的一个应用场景。 这也是Spring框架的最基本的原理,只是它做的更丰富,安全,健壮。 因此,java反射实际上就是动态去加载类并调用类的方法,只需要确定className,methodName这两个字符串就能调用任意一个类的方法,在web里你完全可以传两个参数来动态加载你需要的类。既然有传入就有漏洞,如果className和methodName没有做好过滤那么就会引起RCE。