Java中的HashMap及其与反序列化漏洞的关系


一、HashMap

HashMap 是一种高效存储键值对的数据结构,可以理解为“智能字典”,它的核心设计目标是快速存取数据。下面用通俗易懂的方式讲解它的原理和特点:

1.1 底层结构:数组 + 链表/红黑树

数组(货架)

HashMap 内部有一个数组,每个位置称为“桶”(Bucket),就像仓库里的一排货架。默认有 16 个货架(数组长度),每个货架可以存放多个“包裹”(键值对)。

链表/红黑树(篮子)

如果多个包裹被分配到同一个货架上(哈希冲突),它们会以链表的形式挂在这个货架上。当链表太长(超过 8 个包裹)时,链表会变成红黑树(一种平衡二叉树),这样查找更快。

1.2 存数据的过程:三步到位

  • 1)计算哈希值(包裹编号)

根据键(Key)的 hashCode() 生成一个编号,比如键是 “apple”,计算后得到一个数字。这一步决定了包裹放在哪个货架上。

  • 2)定位货架(找位置)

用哈希值对数组长度取模(或位运算),比如 hash & (n-1),确定具体货架位置。例如,哈希值是 28,数组长度是 16,则放在 28%16=12 号货架。

  • 3)解决冲突(挂篮子)

如果货架是空的,直接放包裹。如果货架已有包裹,挂到链表末尾;链表太长则转红黑树。

1.3 取数据的过程:反向操作

  • 再次计算哈希值

根据 Key 的哈希值找到货架位置。

  • 遍历链表或树

在对应的货架上,逐个比对 Key(用 equals() 方法),找到匹配的包裹。

1.4 关键设计:如何保证高效?

  • 哈希函数优化

哈希算法会将 Key 的高低位混合计算,减少冲突(比如 Java 8 的扰动函数)。

  • 动态扩容(仓库扩建)

默认负载因子是 0.75,比如数组长度 16,当存了 12 个包裹时就触发扩容。扩容会新建一个更大的数组(翻倍),并重新分配所有包裹,这一步较耗时。

  • 红黑树的引入

链表查询慢(O(n)),红黑树查询快(O(log n)),解决极端情况下的性能问题。

1.5 HashMap 的特点

快:理想情况下存取都是 O(1) 时间复杂度。

不保证顺序:数据存放位置由哈希值决定,遍历时无序。

线程不安全:多线程同时修改可能导致数据错乱,需用 ConcurrentHashMap。

允许空键值:可以存储 null 作为 Key 或 Value。

通过这些设计,HashMap 在大多数场景下能实现快速存取,但需注意哈希函数的质量和负载因子的设置,避免频繁扩容影响性能。

二、HashMap与反序列化

HashMap与反序列化的关系可以用“运输快递”来通俗理解。以下是具体解释:

2.1 HashMap的本质与打包过程

HashMap本身是一个存储键值对的容器(类似快递包裹里的物品)。当需要将HashMap对象通过网络传输或保存到文件时,需要将其序列化(即“打包”成二进制字节流)。

序列化条件:HashMap类实现了Serializable接口,因此可以直接通过ObjectOutputStream等工具进行序列化,无需额外操作。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

序列化内容:默认会保存HashMap中的所有非transient字段(如键值对、容量等)。

2.2 反序列化中的拆包裹风险

当接收方收到序列化的HashMap数据后,会通过ObjectInputStream进行反序列化(即“拆包裹”还原对象)。这一过程可能引发安全隐患:

  • 自动触发键的哈希计算

HashMap在反序列化时会调用键(Key)的hashCode()方法,重新计算哈希值以恢复数据结构。

危险点:如果键是恶意构造的对象(如包含触发DNS请求的URL对象),反序列化时会自动执行hashCode()中的危险逻辑。

  • 漏洞利用场景:

例如,攻击者可以构造一个以URL对象为键的HashMap,序列化后发送给目标服务器。当服务器反序列化时,会触发URL的hashCode(),导致DNS查询或更严重的远程代码执行。

2.3 通俗类比

  • 正常场景:

你寄送一个装着普通物品(如书本)的包裹(HashMap)。接收方拆开后,物品完好无损。

→ 对应:序列化传输普通HashMap,反序列化后正常使用。

  • 危险场景:

你寄送一个包裹,里面装了触发式炸弹(恶意键对象)。接收方一拆包裹,炸弹自动爆炸(执行恶意代码)。

→ 对应:反序列化恶意HashMap时触发漏洞。

总结

HashMap的序列化功能使其便于传输和存储,但反序列化时的自动行为(如调用hashCode())可能成为安全隐患。理解这种机制有助于避免“拆包裹炸弹”式攻击。

三、HashMap与反序列化漏洞

Java 反序列化漏洞中,HashMap 是一个常见的“触发点”,它与漏洞的联系主要体现在反序列化过程中通过 HashMap 的重写方法间接执行恶意代码。以下是通俗的解释:

3.1 基础背景:HashMap 在反序列化中的作用

HashMap 的反序列化流程

HashMap 重写了 readObject() 方法,在反序列化时会调用该方法。该方法会依次执行以下操作:

  • 读取键值对数据;
  • 计算每个键的哈希值(调用 hash() 方法);
  • 将键值对存入哈希表。

3.2 漏洞触发点:为什么是 HashMap?

3.2.1 关键调用链

HashMap.readObject() → hash() → 键对象的 hashCode()

在计算键的哈希值时,hash() 方法会调用键对象的 hashCode()。如果攻击者控制键对象,并让该对象的 hashCode() 方法执行危险操作(如 DNS 查询、远程代码执行),就会触发漏洞。

3.2.2 举例:URLDNS 链

  • 构造恶意键对象:用 URL 类作为键。
  • 触发 DNS 请求URLhashCode() 方法会解析 URL 的主机名,触发 DNS 查询。
  • 漏洞验证:攻击者生成一个包含恶意 URL 的 HashMap 序列化数据,当目标反序列化时,会发起 DNS 请求,证明漏洞存在。
// 示例:构造触发 DNS 的 HashMap
HashMap<URL, String> map = new HashMap<>();
URL url = new URL("http://恶意域名.dnslog.cn");
map.put(url, "test");

文章作者: 司晓凯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 司晓凯 !
  目录