老子他妈反复思考
前言
CommonsCollections6没有JDK限制,所以在一些工具中构造命令执行时经常也会用到CC6。本文会学习p神简化得利用链已经分析ysoserial中的代码。
CommonsCollections Gadget Chains | CommonsCollection Version | JDK Version | Note |
---|---|---|---|
CommonsCollections1 | CommonsCollections 3.1 - 3.2.1 | 1.7 (8u71之后已修复不可利用) | |
CommonsCollections2 | CommonsCollections 4.0 | 暂无限制 | javassist |
CommonsCollections3 | CommonsCollections 3.1 - 3.2.1 | 1.7 (8u71之后已修复不可利用) | javassist |
CommonsCollections4 | CommonsCollections 4.0 | 暂无限制 | javassist |
CommonsCollections5 | CommonsCollections 3.1 - 3.2.1 | 1.8 8u76(测8u181也可) | |
CommonsCollections6 | CommonsCollections 3.1 - 3.2.1 | 暂无限制 |
cc6在LazyMap后面和cc1是一样的。
简化版利用链
1 | /* |
解决Java⾼版本利⽤问题,实际上就是在找上下⽂中是否还有其他调⽤ LazyMap#get()
的地⽅。
在类org.apache.commons.collections.keyvalue.TiedMapEntry
中的getValue()
方法中调用了this.map.get
在他的hashCode()
方法中又调用了hashCode()
方法。
1 | public int hashCode() { |
所以,欲触发LazyMap利⽤链,要找到就是哪⾥调⽤了 TiedMapEntry#hashCode
。
在 java.util.HashMap#readObject
中就可以找到 HashMap#hash()
的调用:
在HashMap中的readObject()方法中调用了hash()方法
1 | private void readObject(java.io.ObjectInputStream s) |
查看声明后看到hash()方法调用了hashCode()
在HashMap的readObject⽅法中,调⽤到了 hash(key) ,⽽hash⽅法中,调⽤到了 key.hashCode() 。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前⾯的分析过 程,构成⼀个完整的Gadget。
构造Gadget代码
首先,构造恶意的LazyMap类
1 | Transformer[] fakeTransformers = new Transformer[] {new |
为了避免本地调试时触发命令执行,我构造LazyMap的时候先⽤了⼀个人畜无害的 fakeTransformers
对象,等最后要⽣成Payload的 时候,再把真正的 transformers 替换进去。
然后,拿到了⼀个恶意的LazyMap对象 outerMap ,将其作为 TiedMapEntry 的map属性:
1 | TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey"); |
接着,为了调⽤ TiedMapEntry#hashCode()
,我们需要将 tme
对象作为 HashMap 的⼀个key。注意, 这⾥我们需要新建⼀个HashMap,⽽不是⽤之前LazyMap利⽤链⾥的那个HashMap,两者没任何关系。
1 | Map expMap = new HashMap(); |
然而并没有弹计算器,接下来来进行修改。
原因是在expMap.put(tme, "valuevalue");
步入进去看看
可以看到HashMap#put
也调用了hash()方法。就导致 LazyMap 这个利⽤链在这⾥被调⽤了⼀遍。
我们之前构造的fakeTransformers对象给他的key赋值了,使用非空没能进入这个循环,由于需要trabsform方法来执行命令,故没有弹出计算器。
解决⽅法是将key值为keykey,再从outerMap中移除即可: outerMap.remove("keykey")
修改过后成功弹出计算器
完整poc
1 | import org.apache.commons.collections.Transformer; |
ysoserial利用链
1 | /* |
与上面简化版相比是多了两个方法的调用,分别是java.util.HashSet.readObject()
和java.util.HashMap.put()
POC
1 | package ysoserial.payloads; |
与上面简化版POC不同的是通过反射直接调用putVal新增键值对,随便传入一个hash值,从而避免hashCode的计算。getDeclaredField是根据变量名返回一个成员变量。
重点是HashSet map = new HashSet(1);
调用了HashSet创建了一个对象。
很多try catch应该是为了兼容不同版本的jdk
通过反射获取到HashSet类型对象map的成员变量map
1 | Field f = null; |
通过反射获取到HashSet类型对象map的成员变量table
1 | Field f2 = null; |
获取table的第一个元素(如果为空则获取第二个元素)为node变量
1 | Object node = array[0]; |
获取node的成员变量key并且把key修改为之前构造好的TiedMapEntry类型对象
1 | Field keyField = null; |
这一大段代码的意思就是首先往新的HashSet构造的map添加一个foo,此时map中存了一个值
然后通过反射获取到HashSet类型对象map的成员变量map,并且作为HaspMap的一个Node存到名为innimpl的HaspMap里面
接着通过反射获取到HashSet类型对象map的成员变量table
table是在第一次使用时初始化,并根据需要调整大小。在分配时,长度总是2的幂。(我们也允许某些操作的长度为零,以允许目前不需要的引导机制。)
f2就为HashMap的一个Node,并且名为table,还未被初始化。
get方法 返回指定对象上由此字段表示的字段的值。如果值具有基本类型,则会自动将其包装在对象中。在这里返回了一个被封装HashMap对象key为foo
并且放到数组array里面
并且赋值给一个名为node的Objectle类。
再接着就是获取node的成员变量key并且把key修改为之前构造好的TiedMapEntry类型对象
然后会抛出异常
所以为什么要先再HashSet对象内放个”foo”,然后通过这么多反射,将这个元素修改为entry,直接添加entry不行吗?
看到LazyMap的get方法,通过修改为entry来抛出异常然后没有执行到这里也没有给key赋值,这样在反序列化的时候就会调用super.map.put(key, value);
来接着反序列化链子进行。进而调用Transformer链。
链子动态调试
就看看链子的流程。
断点打在HashSet重写的readObject方法上面,其实这个时候就已经弹计算器了。
步入进入HashMap的get方法
然后调用了HashMap的put方法
接着步入TiedMapEntry的hashCode方法
调用了TiedMapEntry的getValue
然后就是LazyMap的get方法,接下来就是调用Transformer链。我这里因为已经弹过计算器了所以已经反序列化过了所以key有值了,但是链子就是这样走是没问题的,弹计算器也是没问题的。
应该换个地方打断点就可以在弹计算器之前程序停下来了,可是我和她终究没能在正确的时间点遇到。
总结
CC6链子用 java.io.ObjectInputStream.readObject()
–>
java.util.HashMap.readObject()
–>
java.util.HashMap.hash()
–>
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
–>
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
解决了CC1由于sun.reflect.annotation.AnnotationInvocationHandler#readObject
的逻辑变化了而无法调用LazyMap。通过学习这条链子,也锻炼了动态调试的能力。学习更多是学会如何思考反序列化链子,如何处理问题。多多思考,动手看源代码。
参考:
《Java安全漫谈》
Ysoserial Commons Collections6分析 - Zh1z3ven - 博客园 (cnblogs.com)