Skip to content

Latest commit

 

History

History
115 lines (87 loc) · 9.79 KB

File metadata and controls

115 lines (87 loc) · 9.79 KB

HashMap equals 触发器(HashMapEquals)

一句话定位:利用 HashMap.readObject() 反序列化时的哈希桶碰撞,让两个 key 互相 equals(),从而把「Java 原生反序列化」入口转接到「equals 触发」类 gadget。纯 JDK,无第三方依赖。

  • 类别:common 别名:无 优先级:无(未设置)
  • 完整 nameHashMap#readObject()调用equals()方法,注意在调用equals方法之前会调用到包装对象hashCode方法,所以需要保证对象调用hashCode不能出现问题
  • 实现com.ar3h.chains.common.Gadget@Deprecated
  • 作者:无 依赖:无(仅需目标 JVM 的 java.util.HashMap
  • 参数:无 @Param

作用

把下游 gadget 产出的一对对象 {obj1, obj2} 装进一个「被反射篡改过内部结构」的 HashMap。当外层 Payload 对该 HashMap 做 Java 原生序列化再反序列化时,HashMap.readObject() 在重建哈希表的过程中会对两个哈希碰撞的 key 调用 equals(),最终触发 obj1.equals(obj2)(或反向)。它本身不产生危害,只是一个「JavaNativeDeserializeEquals」的入口跳板。

链接标签

  • 入口 tagsJavaNativeDeserialize —— 本 gadget 是链首,承接 Java 原生反序列化入口(如 ObjectInputStream.readObject)。产物是一个 HashMap,可直接交给任意支持原生反序列化的 Payload 入口。
  • 衔接 nextTagsEquals —— 之后必须接一个「入口 tagsEquals」的 gadget,由它提供 chain.doCreate 返回的 Object[]{obj1, obj2},并定义 obj1.equals(obj2) 被调用后要发生什么(继续 toString、getter、JNDI 等)。典型下游:XStringToString1AudioFileFormatAudioFormatRomeEqualsBean1/2
  • excludes:无。

源码剖析

反编译源码本体极短,核心构造逻辑在 PayloadHelper.makeMap(位于 chains-common 模块, 未含该包,下方 makeMap 片段由编译产物 chains-common-1.4.4.jar 的字节码还原,逻辑等价)。

@GadgetTags(tags={"JavaNativeDeserialize"}, nextTags={"Equals"})
@Deprecated
public class HashMapEquals implements Gadget {
    public Object getObject(Object obj1, Object obj2) throws Exception {
        return PayloadHelper.makeMap(obj1, obj2);          // ① 把一对对象塞进特制 HashMap
    }

    @Override
    public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
        Object[] objects = (Object[]) chain.doCreate(context);              // ② 下游产出 {obj1, obj2}
        Object getterObject = context.get(ContextTag.FASTJSON_HANDLE_BYPASS_KEY);
        Object resultObject = this.getObject(objects[0], objects[1]);        // ③ 构造 HashMap
        if (getterObject != null) {
            return CommonMethod.listWrap(getterObject, resultObject);        // ④ Fastjson 绕过时外套 ArrayList
        }
        return resultObject;
    }
}

chain.doCreate(context):本 gadget 约定下游返回 Object[],且必须恰好含两个元素 objects[0]/objects[1]。这正是 nextTags={"Equals"} 的契约——下游 Equals 类 gadget 负责生成「一旦互相 equals 就会继续引爆」的对象对。

③ 特制 HashMap 的构造PayloadHelper.makeMap 字节码还原):

public static HashMap makeMap(Object obj1, Object obj2) throws Exception {
    HashMap map = new HashMap();
    Reflections.setFieldValue(map, "size", 2);                 // 反射伪造 size=2,让 writeObject 认为有两个条目
    Class<?> nodeCls;
    try { nodeCls = Class.forName("java.util.HashMap$Node"); }  // JDK8+ 的桶节点
    catch (ClassNotFoundException e) {
        nodeCls = Class.forName("java.util.HashMap$Entry");     // JDK7 回退
    }
    Constructor<?> ctor = nodeCls.getDeclaredConstructor(int.class, Object.class, Object.class, nodeCls);
    Reflections.setAccessible(ctor);
    Object table = Array.newInstance(nodeCls, 2);              // 长度为 2 的 Node[] 桶数组
    Array.set(table, 0, ctor.newInstance(0, obj1, obj1, null)); // 桶[0] = Node(hash=0, key=obj1, value=obj1)
    Array.set(table, 1, ctor.newInstance(0, obj2, obj2, null)); // 桶[1] = Node(hash=0, key=obj2, value=obj2)
    Reflections.setFieldValue(map, "table", table);            // 反射写入 transient 的 table 字段
    return map;
}

要点:

  • tablesize 都是 HashMaptransient 字段,正常不参与序列化。这里用反射直接写入,是为了让随后的 HashMap.writeObject 能把两个条目写出去。
  • 两个 Node 的构造 hash 均写死为 0,但这个存储的 hash 在反序列化时不会被使用——readObject 会用 key.hashCode() 重新计算(见下)。存储 hash 只是为了合法地把 key/value 挂到桶数组上,使 writeObject 能遍历到它们。
  • keyvalue 都填成同一个对象,序列化时会重复写出,无副作用。

触发原理(在受害端反序列化时发生):外层 Payload 序列化这个 HashMap 时,HashMap.writeObjectsize=2 遍历 table,依次写出 obj1(key)、obj1(value)、obj2(key)、obj2(value)。受害端 HashMap.readObject 逐条读回并调用 putVal(hash(key), key, value, ...)

  1. hash(key) 内部会调用 key.hashCode() —— 这就是 name 里那句警告的由来:equals 之前先触发 hashCode,故下游对象的 hashCode() 必须不抛异常
  2. 只要 obj1obj2 落进同一个桶(下游 gadget 保证二者 hashCode() 相同 / 桶索引相同),第二次 putVal 遍历该桶链时命中判断 p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))), 于是执行 key.equals(k),即 obj2.equals(obj1)(或反向),链条继续向 Equals 下游引爆。

④ Fastjson 绕过分支:当 GadgetContext 中存在 FASTJSON_HANDLE_BYPASS_KEY 时,CommonMethod.listWrap(getterObject, resultObject) 把「getter 对象」与「HashMap」按顺序放入一个 ArrayList 返回(字节码还原:new ArrayList(); add(getterObject); add(resultObject);)。这是为在 Fastjson 等场景下先满足某个 getter 调用、再落到 HashMap 触发点的兼容处理;标准 Java 原生反序列化路径下 getterObject == null,直接返回 HashMap。

参数(@Param)

本 gadget 无 @Param,无用户可配字段。所有语义(要触发谁、equals 之后做什么)都由下游 Equals gadget 决定并通过 chain.doCreate 注入。

字段 名称 说明 默认 可选值
—— —— 无独立可配参数 —— ——

适用版本与原理要点

  • JDK 适配java.util.HashMap$Node 为 JDK8+ 命名,JDK7 桶节点为 HashMap$EntrymakeMaptry/catch 兼容两者,因此覆盖主流 JDK 版本。触发点是 HashMap 本身,无版本白名单顾虑。
  • 无外部依赖dependencies 为空,只要求目标 JVM 存在标准 HashMap(恒成立)。这使它成为极通用的 equals 跳板,等价于 ysoserial CommonsCollections/URLDNS 中经典的「HashMap 哈希碰撞触发 equals」技巧。
  • @Deprecated:类被标注为过时。同族的 HashTableEqualsConcurrentHashMapEqualsHotSTSourceEquals 提供等价或更隐蔽的触发容器;实际生成器可能优先选用它们,但 HashMapEquals 逻辑最简单、最通用。
  • 关键约束(下游需保证)obj1obj2hashCode() 必须 (a) 不抛异常、(b) 使二者落入同一桶,equals 才会被调用。若下游对象 hashCode() 依赖尚未初始化的字段,会在受害端提前抛错导致链失败——这正是 name 中反复强调「保证对象调用 hashCode 不能出现问题」的原因。

所属预设链

usedInChains 为空——自由构件,未直接出现在 51 条预设链中,供按 tags/nextTagsEquals 类 gadget 自由组合。

关联

  • 同族(同为 JavaNativeDeserializeEquals 容器跳板)HashTableEqualsConcurrentHashMapEqualsHotSTSourceEquals(后者额外用 HotSwappableTargetSource 包一层并支持 HessianDeserialize)。
  • 下游(nextTags=Equals,提供 {obj1,obj2} 对)XStringToString1AudioFileFormatAudioFormatXalanXStringToString1/2/3RomeEqualsBean1/2 等——它们把 equals 再转接到 toString/getter,从而衔接后续 sink。
  • 构造辅助com.ar3h.chains.common.util.PayloadHelper#makeMapReflections#setFieldValue/setAccessibleCommonMethod#listWrap

防御与检测

  • 序列化指纹:反序列化流中出现一个 java.util.HashMap,其内部两个 key 对象的 hashCode() 相等且类型为已知 equals gadget(如 XStringAudioFileFormat$TypeAudioFormat$Encoding、rome 的 EqualsBean/ObjectBean)时高度可疑。
  • 调用栈特征:受害端异常/审计栈中出现 HashMap.readObject → HashMap.putVal → hash → <gadget>.hashCode / <gadget>.equals,即命中本模式。
  • 加固建议
    • 使用带类型白名单的反序列化过滤(JEP 290 ObjectInputFilter),拒绝业务无关的 equals 触发类(com.sun.org.apache.xpath.internal.objects.XStringjavax.sound.sampled.*$Type/$Encodingcom.rometools.rome.feed.impl.EqualsBean 等)。
    • 避免对不可信数据做 Java 原生反序列化;如必须,改用无多态的白名单编解码。
    • HashMap 本身无法禁用,防线应放在「equals/hashCode 会引爆的下游类」以及入口过滤上。