内容纲要

前言

本章我们来看一看 Apache CommonsCollections6 Payload 的构造与利用.


Apache CommonsCollections6

首先来看一看 YSoSerial 中关于 ACC6 Payload 的定义 .

通过上图不难看出 , ACC6 Payload 的核心为通过 TiedMapEntry.getValue() 方法来调用 LazyMap 攻击链 . 这种构造手法与 ACC5 Payload 完全相同.

但 ACC6 Payload 用了一种新的手段来调用 TiedMapEntry.getValue() 方法 , 其过程类似 URLDNS Payload , 我们还需要进一步分析函数调用栈.


Apache CommonsCollections6 Payload 复现

分析之前,还是先来复现一遍该利用链.

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

  1. 添加参数 , 生成可利用的 ACC6 Payload .

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

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

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


Apache CommonsCollections6 Payload 构造

构造生成 ChainedTransformer / LazyMap / TiedMapEntry 实例对象

这一部分我们应该非常熟悉了 .


构造生成 HashSet 实例对象

  1. 首先我们要了解 HashSet.Class 的定义.

    HashSet.Class 扩展了 AbstractSet 接口并实现了 Set 接口 , 它创建了一个使用 HashMap 进行存储的没有重复元素的集合

  2. 在 YSoSerial 中 , 程序在生成 HashSet 实例对象的同时指定了 HashSet 中 HashMap 的初始容量大小 .

    由于 HashMap 的容量被指定为 1 , 因此需要调用 map.add() 方法来填充 HashSet 实例对象 , 同时定义了一个空 Field 对象.

    然后通过 Java 反射机制来获取 HashSet.map 对象( 如果是 Android 环境则获取 HashSet.backingMap 对象 ) , 由于该字段通过 private 修饰符修饰 , 因此需要通过 setAccessible(true) 方法来开启访问权限.

  3. 在Java反射中 Field 对象用于获取某个类的属性 , 这里通过 field.get() 方法来获取 Field 对象中字段 map 的值

    由于 HashSet.map 类型为 HashMap.Class , 我们获取了一个 HashMap 实例对象.

  4. 通过反射获取 HashMap.table 字段 , 如果是 Android 环境则获取 HashMap.elementData 字段

    由于 HashMap.table 类型为 Entry.Class , 因此我们可以获取一个 HashMap$Entry 实例对象

  5. 创建 HashMap$Entry 类型的变量 node , 并通过 Java 反射机制获取 HashMap$Entry.key 字段 , 如果是 Android 环境则获取 java.util.MapEntry.key 字段

    当获取了对应字段后 , 程序会通过 setAccessible(true) 方法开启字段访问权限 , 并通过 Field.set() 方法来修改对应字段的值..

    此处获取的字段为 node.key , 即 HashMap$Entry.key 字段 , 因此我们可以直接修改 HashMap$Entry.key 字段的值.

因此 , 这一系列反射赋值的最终目的就很明显了 : 将 HashMap$Entry.key 字段指向 TiedMapEntry 实例对象.


Apache CommonsCollections6 Payload 利用

现在来调试一下反序列化时的函数调用链


HashSet.readObject()

由于 Payload 构造时返回的实例对象为 HashSet 类型 , 因此反序列化时会进入 HashSet.readObject() 方法

该方法先获取容量( Capacity )和装载因子( LoadFactory ) , 初始化并创建 HashMap 实例对象 , 并向其中填值.

在向 HashMap 中赋值时 , 会再次调用 readObject() 方法 , 由于字段 foo 的类型在构造 Payload 时被修改为 TiedMapEntry , 所以此处 readObject() 方法会返回 TiedMapEntry 实例对象.

最终 , 程序会将 TiedMapEntry 对象作为 HashMap 的键名来调用 HashMap.put() 方法


HashMap.put()

HashMap 在赋值时会先计算键名的 Hash 值 , 因此会调用 HashMap.hash() 方法.


HashMap.hash()

该方法主要通过 hashCode() 方法来计算 Hash 值 .

注意此处调用的是 : TiedMapEntry.hashCode() 方法


TiedMapEntry.hashCode()

该方法会中会调用 TiedMapEntry.getValue().


TiedMapEntry.getValue()

在 Payload 中生成 TiedMapEntry 实例对象时 , 我们已经将 TiedMapEntry.map 字段指向 LazyMap 对象了 , 因此这里会直接进入 LazyMap.get() 调用链.

接下来的步骤我们太熟悉了 , 最终会通过 java.lang.Runtime.getRuntime().exec() 方法来执行系统命令


总结

先贴一下完整的函数调用链

Apache CommonsCollections6 与 Apache CommonsCollections5 整体思路是相同的 , 唯一的区别在于调用 TiedMapEntry.getValue() 方法的方式.

  • Apache CommonsCollections5

    BadAttributeValueExpException.readObject() ->
    TiedMapEntry.toString() ->
    TiedMapEntry.getValue ->
    LazyMap.get() -> ... ... ...

  • Apache CommonsCollections6

    HashSet.readObject() ->
    HashMap.put() ->
    HashMap.hash() ->
    TiedMapEntry.hashCode() ->
    TiedMapEntry.getValue() ->
    LazyMap.get() -> ... ... ...

理解了上面这个点 , 整个 ACC6 Payload 就很清晰了 !

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

作者

留言

撰写回覆或留言

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