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方法调用,以及中间设置值绕了好一会,细节还是比较多,但理清过后还是比较容易理解,学习中间发现还有另外的延伸,能举一反三才是真正的进步。