0x00、前言
CC7的触发点同CC1一样,采用ChainedTransformer链和LazyMap类,入口点变了,跟前面CC1~6不一样,但逆向思维去分析查找链实在没办法,效果跟正向分析没啥区别了,所以主要以正向分析去学习,还是单独整理出来吧
0x01、Apache Commons Collections描述
引用CC1链分析中的描述
CC链即Commons Collections利用链,主要针对Commons Collections组件发现的利用链。
Apache Commons是Apache软件基金会的项目。Commons的目的是提供可重用的、开源的Java代码。
Apache Commons提供了很多工具类库,他们几乎不依赖其他第三方的类库,接口稳定,集成简单,可以大大提高编码效率和代码质量。
Apache Commons Collections 是对 java.util.Collection 的扩展。
Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
目前 Collections 包有两个 commons-collections 和commons-collections4,commons-collections 最新版本是3.2.2,3系列版本也是只有3.2.2是安全的,不支持泛型,目前官方已不在维护。collections4 目前最新版本是4.4,其中4.0是存在漏洞,最低要求 Java8 以上。相对于 collections 来说完全支持 Java8 的特性并且支持泛型,该版本无法兼容旧有版本,于是为了避免冲突改名为 collections4。推荐直接使用该版本。(注:两个版本可以共存,使用时需要注意)
0x02、环境准备
java版本:jdk8u66(版本无限制)
Commons Collections:3.2.1(漏洞版本在3.1-3.2.1)
maven项目pom.xml文件中添加依赖
<dependencies> |
在idea访问Commons Collections
组件的文件时候点击上方的下载源代码就可以看到对应文件的.java文件了
0x03、分析
CC7链也利用ChainedTransformer
链和LazyMap
类作为触发点,入口不一样,目的都是为了调用lazyMap.get()
方法
回顾
回顾LazyMap触发点:
- 生成
LazyMap
对象,将InvokerTransformer
利用方法串起来的ChainedTransformer
对象传入LazyMap
构造方法。 lazyMap.get()
方法会调用ChainedTransformer.transform()
执行链转换器内的Runtime.exec()
方法达到命令执行的目的。
CC1~6中使用到LazyMap的链:
CC1(LazyMap链):
- 生成
LazyMap
对象,将InvokerTransformer
利用方法串起来的ChainedTransformer
对象传入LazyMap
构造方法,随后将LazyMap
对象传入AnnotationInvocationHandler
代理类。 - 通过动态代理,在生成二次代理对象时调用对象的
invoke
方法,其中invoke
方法中调用LazyMap.get()
方法、get()
方法调用ChainedTransformer.transform()
方法最后实现InvokerTransformer.transform()
执行命令;
CC5:
- 生成
LazyMap
对象,将InvokerTransformer
利用方法串起来的ChainedTransformer
对象传入LazyMap
构造方法。 - 将
LazyMap
对象传入TiedMapEntry
类构造方法,再通过TiedMapEntry.toString()
方法去调用TiedMapEntry.getValue()
方法,最后调用到lazyMap.get()
方法。 - 将
TiedMapEntry
对象通过反射赋值给BadAttributeValueExpException
类的val
值,通过反序列化BadAttributeValueExpException
类对象,调用重写readObject
方法中的val.toString()
。 val.toString()
调用等效于TiedMapEntry.toString()
,即调用TiedMapEntry.getValue()
方法,达到调用lazyMap.get()
方法触发代码执行。
CC6:
- 生成
LazyMap
对象,将InvokerTransformer
利用方法串起来的ChainedTransformer
对象传入LazyMap
构造方法。 - 将
LazyMap
对象传入TiedMapEntry
类构造方法,再通过TiedMapEntry.hashCode()
方法去调用TiedMapEntry.getValue()
方法,最后调用到lazyMap.get()
方法。 - 通过使用
hashmap
的put
方法添加元素时调用hash(key)
方法,进而调用key.hashCode()
方法,将TiedMapEntry
对象作为keyput
入hashmap
中,达到调用TiedMapEntry.hashCode()
的目的(hashSet同理,本质上都是调用hashmap)。
分析
回到lazyMap.get()
方法本身上来,CC7使用了Hashtable
作为入口,也就是说对Hashtable
对象进行序列化,然后在反序列化时触发漏洞
简述Hashtable哈希表:
Hashtable是原始的java.util的一部分, 是一个Dictionary具体的实现 。
然而,Java 2 重构的Hashtable实现了Map接口,因此,Hashtable现在集成到了集合框架中。它和HashMap类很相似,但是它支持同步。
像HashMap一样,Hashtable在哈希表中存储键/值对。当使用一个哈希表,要指定用作键的对象,以及要链接到该键的值。
然后,该键经过哈希处理,所得到的散列码被用作存储在该表中值的索引。
同hashmap
一样,都是存储键值对<key,value>的散列表。
Hashtable
同样重写了序列化和反序列化过程
hashtable反序列化过程
序列化(writeObject)过程,其实查看英文注解就大概知道这个过程了:
private void writeObject(java.io.ObjectOutputStream s) |
序列化过程简单来说就是创建一个Entry
类型变量entryStack
来读取table
中的entry
数据,再依次读取entryStack
中的entry
元素中的key
和value
写入到流中。
反序列化(readObject)过程
private void readObject(java.io.ObjectInputStream s) |
反序列化过程相对也比较好理解,主要就是从流中读取到原始信息,再重新计算长度去创建一个新table
表,并读取流中key
和value
通过reconstitutionPut
方法写入到table
表中
反序列化过程主要只调用了reconstitutionPut
方法,该方法比较关键,再跟进reconstitutionPut
方法
reconstitutionPut()
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) |
同样通过注解也很好理解步骤(主要就是value不允许为Null,然后key值不能重复,元素hash不能相同,也是hashtable的特性)
- 先判断value值,如果value为空,就抛出序列化异常
- 计算key的hash,再根据hash和table长度计算出存储索引index,并判断该key的hash或者该key的值是否在表中存在,如果已存在就抛出序列化异常
- 经过判断没问题后 就将key value写入到Entry中
reconstitutionPut
方法中调用了key.hashCode()
方法和e.key.equals(key)
方法,其中key.hashCode()
方法在下面POC延伸中会分析,现在还有e.key.equals(key)
方法,在for循环中,判断当前元素的key
的hash
在表中是否存在,并且会对元素的key
对表中元素的key
进行equals()
匹配查看key
是否在表中存在。
跟进e.key.equals(key)
方法,其中e.key
表示从流中读取的table
表中的key
,也就是我们put
进的key
值。
该链通过Hashtable
调用Lazymap
,那传入的key
值肯定就为Lazymap
对象。e.key.equals(key)
调用相当于e.Lazymap.equals(key)
满足条件:
- value不能为空,设置value值就行。
- 需要
index
相同且e.hash == hash
,也就是在table表中需要有两个元素的hash是相等的,hash相等才能获取到table中的元素,才能去执行e.key.equals(key)
这个方法,因此必须put进两个hash相等的元素。
AbstractMapDecorator.equals()
跟进Lazymap.equals()
方法,但Lazymap
类本身没有equals()
方法,实际是调用它的父类AbstractMapDecorator
下的equals()
方法
其中会判断object
对象是否为同一个对象,是的话返回true
,如果不是同一个对象,就执行map.equals(object)
,其中的map
为Lazymap
传参的map
,也就是hashmap
对象
继续跟进hashmap.equals()
方法,同样hashmap
本身只有在它的Node
子类中存在equals()
方法,实际调用的也是父类下的equals()
方法
AbstractMap.equals()
public boolean equals(Object o) { |
满足条件:
- 不能为同一个对象,如果两个元素相同就直接返回true
- 该对象类型必须为Map类型,不是Map类型的话返回false
- 对象元素个数必须相同,不同的话返回false
满足以上条件后,由于本身value
不能为空(为空就不会进入到这个调用阶段了),因此会执行if (!value.equals(m.get(key)))
判断体中的m.get(key)
方法,而m
对象由传入的Object
对象,也就是上面分析传入对比equals()
方法中的对象,即Lazymap
对象,实际调用Lazymap.get(key)
,达到代码执行的目的。
POC构造
上面几个关键的方法分析完了,从POC构造继续串联起来分析
首先触发点不变
Transformer[] transformers = new Transformer[]{ |
接着就是Lazymap
的传值和对hashtable
之间的衔接,在上面方法的分析中提到reconstitutionPut()
方法中需要满足
value
不能为空,设置value
值就行。- 需要
index
相同且e.hash == hash
,也就是在table
表中需要有两个元素的hash
是相等的,hash
相等才能获取到table
中的元素,才能去执行e.key.equals(key)
这个方法,因此必须put
进两个hash
相等的元素。
由上满足条件,我们传入两个相同的元素
Map map = new HashMap(); |
但会遇到问题,如果传入两个相同的元素,由于hashtable
的put
操作
其中针对第一个元素put
入的时候,此时Table
不存在其他元素,此时index
指针获取的table
为null
,不会进入for
循环体,直接进入addEntry()
方法中
addEntry()
方法将元素添加对应元素hash
计算出来的index
指针的table
中,最后计数+1,表示table
中新增了一个元素个数
如果添加两个一模一样的元素,就会导致在第二次put
对象时,由于hash
相同,计算的index
相同,key
值相同,进入到put
方法的for
循环中返回了,自然不会进行addEntry()
添加操作,此时table
表中元素个数仍为1。
在反序列化获取elements
元素个数时,此时也为1,只进行一次reconstitutionPut
方法调用。而需要调用第二次reconstitutionPut
方法,判断里面的hash
相同才能运行到e.key.equals(key)
方法。
因此需要我们传入两个元素的hash相同,但是元素key不相同的两组元素
由上满足条件,我们传入两个元素的hash相同,但是元素key不相同的两组元素
Map map = new HashMap(); |
再解决本地触发的问题,由于在第二次put
中会进入到循环体中执行e.key.equals(key)
方法,会调用到transformerChain.transform()
达到执行代码。
规避这个问题还是用老方法,先传入空的转换器进Lazymap
对象中,put
完值后再通过反射修改转换器。
//创建一个空的链转换器 |
由于put
第二次的时候e.key.equals(key)
中的e.key
为第一次元素的Key
,目的是拿第二次put
的key
查看是否跟表中的key
是否相等,而参数中的key
(第二次put入的key值)为最后调用的m.get()
中的m
对象,因此只需要修改第二个传入的lazymap
对象的factory
即可。
还有个点就是由于在put
入第二个entry
元素时,调用了e.key.equals(key)
方法,最后调用到m.get(key)
即Lazymap.get(key)
方法
由于转换器制空后,本地不会触发代码执行,但会进行map.put(key, value);
,将第二个元素的key
进行put
操作,此时Lazymap2
又新增了一个key
为yy
的元素,因此需要删除
Lmap2.remove("yy"); |
最后将hashtable
对象进行序列化,反序列化的时候触发漏洞,得到完整poc:
public static void main(String[] args) throws Exception { |
CC7-POC延伸
反序列化(readObject)过程中提到的reconstitutionPut
方法
在上面反序列化过程reconstitutionPut
方法写入entry
中发现一个敏感的key.hashCode(),在cc6的学习中就是利用hashmap.put(key,value)
调用hash(key)
方法最后调用key.hashcode()
进行调用达到代码执行。
同样在这里,key值可控,也是通过hashtable
对象put
入的key
值进行hashCode()
操作,跟cc6基本没差别,这就不再复分析一遍了。
得到一个延伸的poc
触发点不变
Transformer[] transformers = new Transformer[]{ |
通过lazymap
调用触发点
Map map = new HashMap(); |
再通过TiedMapEntry
类最后调用Lazymap.get()
TiedMapEntry TM=new TiedMapEntry(Lmap,11); |
最后使用hashtable
的put
方法添加元素TiedMapEntry
对象作为入口,通过反射修改table
表key
值去规避本地执行,同样可以通过反射修改链转换器,操作跟cc6分析写Poc的一样,这里就用反射修改key
值了。
Hashtable ht=new Hashtable(); |
然后序列化,反序列化触发漏洞,得到完整POC:
public static void main(String[] args) throws Exception { |
执行效果:
0x04、总结
CC7相对于前面几个链,更换了入口,其中多层的equals方法调用,以及中间设置值绕了好一会,细节还是比较多,但理清过后还是比较容易理解,学习中间发现还有另外的延伸,能举一反三才是真正的进步。