内容纲要

前言

本章我们来分析 YSoSerial 内置的最后一个 Apache CommonsCollections Payload : CommonsCollections7

ACC7 这条 POP Chian 是非常有意思的 , 涉及到很多新的知识点 , 下面我们就来学习一波.


CommonsCollections7

CommonsCollections7 Payload 复现

复现环境为 : JDK1.7_67 + Tomcat7 + CommonsCollections-3.1

  1. 首先添加参数并生成可用的 ACC7 Payload

  2. 加载 CommonsCollections-3.1 组件并启动 Tomcat7 , 将本地生成的 Payload 通过 Curl 工具发出.

    curl "http://localhost:8080/webdemotest_war_exploded/demotest" --data-binary @/tmp/payload.ser

    Payload 被成功执行 , 漏洞复现成功.


CommonsCollections7 Payload 简介

在分析 Payload 构造之前先来看一看复现时的报错信息.

抛出的异常为 : java.io.StreamCorruptedException , 当从对象流中读取的控制信息与内部一致性检查相冲突时 , 通常会抛出此异常.

我们根据函数调用栈直接跳转到 java.util.Hashtable.reconstitutionPut() 方法中.

这里会判断 Key 值是否重复 , 如果重复则会抛出上述异常. 既然生成的 Payload 能够利用成功 , 那么说明服务端在抛出异常前已经进入了 POP Chains . 该类主要调用了 hash()equals() 两个方法 , POP Chains 的跳转肯定是其中一个方法触发的.


CommonsCollections7 Payload 构造

构造 ChainedTransformer - ConstantTransformer - InvokerTransformer 反射调用链

这一步我们已经非常熟悉了 , 就不深入分析了.

当服务端调用 ChainedTransformer.transform() 方法时 , 系统会去反射调用 java.lang.Runtime.getRuntime().exec() 方法 , 执行系统命令


构造并填值两个 LazyMap 实例对象

这里注释写的很清楚 : 创建两个带有冲突散列的 LazyMap 实例对象 , 使服务端在 readObject() 期间强制进行元素比较.

从注释中我们可以得到两个关键信息.

  • CommonsCollections7 Payload 利用了 Hash冲突( Hash碰撞 ) 机制
  • CommonsCollections7 Payload 的利用点位于元素比较 , 漏洞触发点很可能就是 reconstitutionPut() 方法中对 equals() 方法调用

什么是 Hash冲突( Hash碰撞 )

上文提到了一个新名词 : Hash冲突( 或者叫 Hash碰撞 ) , 那么到底什么是 Hash冲突呢 ?

说实话 , Google 上搜了一圈还真没找到官方定义 , 不过大多解释都是类似的 . 个人认为 ProgrammerSought 上关于 Hash冲突 的定义是比较准确的.

简单的说 , Hash冲突是指两个不同的 key 通过 hash() 方法计算出同一个 Value .

结合上一步返回的两个 LazyMap 实例对象 , 我们就能发现冲突的地方

不同的 Key 计算出了相同的 Hash 值( 4044 ) , 此处会触发 Hash冲突( Hash碰撞 ).


如何构造 Hash冲突( Hash碰撞 )

为什么不同的 Key 能计算出相同的 Hash 呢? 这其实是前辈们刻意构造的 , 整个过程非常的精彩 !.

而要想理解整个过程 , 则需要去分析 HashMap.hash() 方法的源码 !

  1. HashMap.hash()
    指标
    该方法会获取要计算 Hash 值的键名 , 对该键名及 HashSeed 进行一些判断 , 再调用 hashCode() 方法来计算键名的 Hash 值.

    程序还会对 hashCode() 方法的计算结果做一些移位处理 , 将处理后的 Hash 值返回 . 这一步不涉及对键名操作 , 因此只要 hashCode() 方法的计算结果相同 , 最后返回的 Hash 值也相同.

    因此 , 我们分析的重心应该放在 hashCode() 方法中.

  2. String.hashCode()

    由于传入的键名( 例如 "yy" )是 String.class 类型 , 所以程序会调用 String.HashCode() 方法

    这个计算过程其实比较简单 , 调试几遍后就能发现如下规律

    字符串 "AB" , Python环境下可以用 ORD("A") + ORD("B") 来表示 , 我们令 ORD(A) = X1 , ORD(B) = X2

    Hash值的计算可以按照如下公式 :

    我数学比较渣== 其实应该可以得到个公式 , 但我没去弄了 , YSoSerial ACC7 Payload 中指定的键名为 "yy" 和 "zZ" , 看这个例子您就明白了

    已知 : ord("y") == 121 , ord("z") == 122 , ord("Z") == 90

    "yy".hashCode() == 31 × 121 + 1 × 121 == 3872

    "zZ".hashCode() == 31 × 122 + 1 × 90 == 3872

    可得 : "yy".hashCode() == "zZ".hashCode() == 3872

    最终返回的 Hash 值 4044 自然也相同了

  3. 其他可利用的 Hash冲突

    这里其实就很多了 , 字符串位数也不唯一 , 举个例子

    假设需要一个三位的字符串 , 化简后应该满足如下公式

    随便取一些满足条件的值 , 其对应 ASCII 字符如下:

    X1 = 109 , Y1 = 110
    X2 = 100 , Y2 = 69
    X3 = 105 , Y3 = 105

    现在 , 将 "mdi" 和 "nEi" 两个字符串作为参数填入到 LazyMap 中 , 调试时可以看到键名对应的 Hash 值是相同的

    生成的 Payload 同样可以攻击成功.

    需要注意的是 : 在调用 LazyMap.put() 方法时 , 填入的 Value 也需要相同 , 不过原因我们放到后面再说.


填充并构造 HashTable 实例对象 , 触发 Hash冲突( Hash碰撞 )

下面代码会构造一个 Hashtable 实例对象 , 并通过 Hashtable.put() 方法向其中赋值 . 这一步的核心目的是触发 Hash冲突( Hash碰撞 ).

前文说过 Hash冲突 是指不同的 Key 计算出相同的 Hash Value , 因此解决 Hash冲突 的核心就是解决 Hash Value 值的冲突问题 .

通常情况下有两种方法来解决 Hash冲突.

  • 单向链表法 : 将相同Hash值的对象构造成一个单向链表并存放在对应的槽位.
  • 开放地址法 : 当某个槽位已经被占据的情况下 , 通过一个探测算法来查找下一个可以使用的槽位.

HashMap 使用 "单向链表法" 来解决 Hash冲突 , 其核心代码如下 :

程序会将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处 , 即 table[bucketIndex].

如果 table[bucketIndex] 索引处已经有了一个 Entry 对象 , 按么新添加的 Entry 对象将指向原有的 Entry 对象( 即生成一个单向链表) .

如果 table[bucketIndex] 索引处没有 Entry 对象 , 那么新放入的 Entry 对象将指向 null , 不会生成单向链表.


Hash冲突( Hash碰撞 ) 的流程
  1. HashTable.put()

    现在 , 我们直接来分析一下 YSoserial 是如何触发 Hash冲突的 . 跟进代码 , 进入 HashTable.put() 方法.

    该方法同样会调用 hash() 方法计算键名对应的 Hash值 .

  2. HashTable.hash()

    HashTable,hash() 方法会调用 hashCode() 方法来调用计算 Hash值 , 注意这里要调用的是 LazyMap.hashCode() 方法.

    但是查看 LazyMap.class 类定义 , 指标发现该类中并没有实现 hashCode() 方法

    因此 , 这里实际调用的是 LazyMap 父类的 AbstractMapDecorator.hashCode() 方法.

    通过代码可见 , 最终程序会调用 HashMap.hashCode() 方法 .

  3. HashMap$Entry.hashCode()

    该方法会分别对 key 与 value 调用 String.hashCode() 方法 , 并将二者返回结果异或运算 , 得到最终的 Hash 值.

    这里与前文类似 , 就不再重复分析了 . 不过这里可以解答前文一个问题.

    在调用 LazyMap.put() 方法时 , 填入的 Value 也需要相同.

    不难看出 , HashMap 也会对 Value 进行 Hash 运算 , 因此 key 的 Hash值 要相同 , Value 的 Hash值 也要相同 . 最简单的方法就是取两个相同的 Value.

    当然这里只要两个 Value 的 Hash值 相同也是可以的 , 例如下面这组参数生成 Payload 也是可以利用成功的.

  4. 从 Hash冲突( Hash碰撞 )点 到 LazyMap 利用链

    这段代码是为了判断新加入的键名是否已经在 HashTable 中 , 也就是判断是否出现 Hash冲突 .

    第一次执行 hashtable.put(lazyMap1, 1); 时由于 Hashtable 为空 , 所以不会进入 for 循环 . 而第二次执行 hashtable.put(lazyMap2, 2); 时 , 由于 HashTable 中已有键 , 所以会对键名进行比较.

    在比较时会调用 equals() 方法 , 所以这里函数进入了 LazyMap.equals() 方法中

    LazyMap.class 没有 equals() 方法 , 所以这里实际上调用的是其父类 AbstractMapDecorator.equals() 方法

    AbstractMapDecorator.equals() 方法中会去调用 HashMap.equals() 方法

    类似的 , HashMap.class 同样没有 equals() 方法, 所以这里实际上调用的是其父类 AbstractMap.equals() 方法

    AbstractMap.equals() 方法中 , 可执行到一步 m.get(key) 的方法调用 . 此时 m 指向 LazyMap 实例对象 , 即调用 LazyMap.get() 方法

    终于进入到了我们熟悉的 LazyMap 利用链 . 只要对 LazyMap 实例对象进行调试 , 就可以触发反序列化漏洞 !

至此 , 我们已经知道了如何从 Hash冲突( Hash碰撞 )点执行到 LazyMap 利用链. 当 HashTable 构造完毕后 , 我们就能看到前文提到的单向链表 .


向 transformerChain.iTransformers 字段注入构造好的恶意 transformers 对象

这里我们已经很熟悉了, 因此不再深入分析.


移除 LazyMap2 中的单向链表

在构造 Payload 时我们已经触发了 Hash冲突( Hash碰撞 ) , 只是由于恶意代码没有被注入到 transformerChain 对象中 , 因此没有触发命令执行.

而由于 HashMap 的单向链表机制 , 此时 lazyMap1 和 layMap2 已不再相同.

而要想在反序列化时触发 Hash冲突 , 就必须让 lazyMap1 和 lazyMap2 相同 . 因此这一步的目的是移除构造 Payload 时生成的单向链表 , 使得序列化数据在反序列化时能够触发 Hash冲突( Hash碰撞 )

现在 lazyMap1 和 lazyMap2 都已经复原了 , 可以再次触发 Hash冲突.

至此 , 整个 Apache CommonsCollections7 Payload 就已经分析完成.


CommonsCollections7 Payload 利用

其实在讲构造时已经把 ACC7 Payload 的利用链讲的很清楚了 , 服务端利用的场景也就是带入恶意的 transformerChain 实例对象再走一遍 Hash冲突( Hash碰撞 ) 的过程.

所以这里贴个函数调用栈 , 您就大概明白了.

通过前面对 Payload 的分析 , 服务端的分析没有任何难度 , 就不再详细说明了.


总结

不知不觉 Apache CommonsCollectons 7 条 POP Chains 就已经全部分析完了 . 分析期间借鉴了很多前辈们的思路 , 感谢前辈们的付出.

这段时间我学到了很多很多东西 , 从一个连 Java HelloWorld 都不会写的人到能把 Apache CommonsCollections Payload 调试明白 , 最后的结果我还是挺满意的~

[ 解密 YSoSerial ] 这个系列我还会继续写下去 , 期待有一天我能把所有的 Payload 和 Exploit 分析完吧 .

最后修改日期:2020年9月27日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。