时效性
2025年8月4日
题目来源
题目描述
Write UP
只要通过碰撞,获得一个md5值为0e开头,并且后续值都为整数(注意这个是必要条件)的字符串即可
总结
这里涉及到一个 == 弱比较的绕过方式
在 PHP 中,== 是弱类型比较(松散比较) 运算符,它在比较时会尝试进行隐式类型转换,可能导致非直观的结果。以下是关键特性和常见陷阱:
核心规则:
-
类型转换优先:比较前先将操作数转换为相同类型(通常是数字或布尔值)。
-
数字与字符串比较:字符串会被转为数字(从左截取有效数字部分,失败则为
0)。123 == "123abc" // true("123abc" → 123) 0 == "abc" // true("abc" → 0) "1e3" == "1000" // true(科学计数法 → 1000) -
布尔值比较:
true转为1,false转为0。true == "1" // true("1" → 1) false == "" // true("" → 0 → false) false == "0" // true("0" → 0 → false) -
null 比较:
null与空值/零相等。null == 0 // true null == false // true -
数组比较:空数组
[]等于false或null。[] == false // true [] == 0 // true([] → 0)
经典安全陷阱(哈希碰撞):
"0e123" == "0e456" // true(双方转为科学计数法 0)
此特性曾被用于绕过密码验证(如 MD5("240610708") = "0e462...")。
严格比较解决方案:
使用 ===(强类型比较)避免隐式转换:
php
123 === "123" // false(类型不同)
false === "" // false
null === 0 // false
| 比较示例 | == 结果 | 原因 |
|---|---|---|
"123" == 123 | true | 字符串转数字 |
"abc" == 0 | true | 字符串转数字为 0 |
true == "1" | true | 字符串转数字,布尔转数字 |
false == null | true | 双方转为 0 |
"0e1" == "0e2" | true | 科学计数法均为 0 |
[] == false | true | 空数组转为 0 |
该PHP代码要求通过GET参数a提供一个字符串,该字符串需满足以下条件:
- 不等于
'QNKCDZO'。 - 其MD5哈希值在松散比较(
==)下等于md5('QNKCDZO')的值。
md5('QNKCDZO')的值为0e830400451993494058024219903391。在PHP的松散比较中,如果一个字符串以0e开头且后跟全数字,它会被解释为科学计数法的0(即数值0)。因此,任何MD5哈希值以0e开头且后跟全数字的字符串,在松散比较下都会等于0e830400451993494058024219903391。
已知字符串'240610708'的MD5哈希值为0e462097431906509019562988736854,该值以0e开头且后跟全数字。同时,'240610708'不等于'QNKCDZO'。
因此,将a设置为240610708可满足条件:
$a = '240610708',不等于'QNKCDZO'(条件1满足)。$md52 = md5('240610708') = '0e462097431906509019562988736854'。$md51 = md5('QNKCDZO') = '0e830400451993494058024219903391'。- 在松散比较下,
$md51 == $md52为true,因为两者均被解释为数值0(条件2满足)。
访问URL时,将a参数设置为240610708,例如:script.php?a=240610708,即可输出flag。
下面分享一个碰撞脚本,可以获取对应的字符串
import hashlib
import argparse
import multiprocessing
import sys
import itertools
import string
import time
def generate_candidates(charset, length):
"""生成指定字符集和长度的所有可能字符串组合"""
for candidate in itertools.product(charset, repeat=length):
yield ''.join(candidate)
def worker(task_queue, result_queue, prefix, charset, max_length):
"""工作进程函数,用于检查候选字符串是否满足条件"""
while True:
try:
# 从任务队列获取下一个要检查的长度
length = task_queue.get(timeout=1)
if length is None: # 终止信号
break
# 生成并检查该长度的所有候选字符串
for candidate in generate_candidates(charset, length):
# 计算MD5哈希
hash_val = hashlib.md5(candidate.encode()).hexdigest()
# 检查是否满足两个条件:
# 1. 以指定前缀开头
# 2. 除前两位外其余字符都是数字
if (hash_val.startswith(prefix) and
len(hash_val) > 2 and
all(c in '0123456789' for c in hash_val[2:])):
result_queue.put((candidate, hash_val))
return # 找到结果后退出进程
except multiprocessing.Queue.Empty:
continue
def find_md5_prefix(prefix, charset=string.digits + string.ascii_letters, max_length=6, num_processes=None):
"""
查找MD5哈希以指定前缀开头且后续字符全为数字的字符串
参数:
prefix: 目标MD5前缀(十六进制小写字符串)
charset: 候选字符串字符集
max_length: 要搜索的最大字符串长度
num_processes: 使用的进程数(默认使用所有CPU核心)
"""
# 验证输入
if not all(c in '0123456789abcdef' for c in prefix):
raise ValueError("前缀必须为十六进制小写字符")
if num_processes is None:
num_processes = multiprocessing.cpu_count()
# 创建进程间通信队列
task_queue = multiprocessing.Queue()
result_queue = multiprocessing.Queue()
# 填充任务队列(按字符串长度)
for length in range(1, max_length + 1):
task_queue.put(length)
# 添加终止信号
for _ in range(num_processes):
task_queue.put(None)
# 创建并启动工作进程
processes = []
for _ in range(num_processes):
p = multiprocessing.Process(
target=worker,
args=(task_queue, result_queue, prefix, charset, max_length)
)
p.start()
processes.append(p)
# 等待结果
try:
result = result_queue.get(timeout=3600) # 1小时超时
except multiprocessing.Queue.Empty:
result = None
# 终止所有进程
for p in processes:
p.terminate()
return result
if __name__ == "__main__":
# 设置命令行参数解析
parser = argparse.ArgumentParser(
description='查找MD5哈希以指定前缀开头且后续字符全为数字的字符串'
)
parser.add_argument('prefix', type=str, help='目标MD5前缀(十六进制小写字符串)')
parser.add_argument('-c', '--charset', type=str, default='alnum',
choices=['digits', 'letters', 'alnum', 'all'],
help='候选字符集: digits(数字), letters(字母), alnum(字母数字), all(所有可打印字符)')
parser.add_argument('-l', '--max-length', type=int, default=6,
help='最大搜索字符串长度(默认:6)')
parser.add_argument('-p', '--processes', type=int,
help='使用的进程数(默认使用所有CPU核心)')
args = parser.parse_args()
# 根据参数选择字符集
charset_map = {
'digits': string.digits,
'letters': string.ascii_letters,
'alnum': string.digits + string.ascii_letters,
'all': string.digits + string.ascii_letters + string.punctuation
}
selected_charset = charset_map[args.charset]
print(f"开始搜索: MD5以'{args.prefix}'开头且后续字符全为数字的字符串")
print(f"字符集: {args.charset}, 最大长度: {args.max_length}")
start_time = time.time()
try:
result = find_md5_prefix(
prefix=args.prefix,
charset=selected_charset,
max_length=args.max_length,
num_processes=args.processes
)
if result:
candidate, hash_val = result
print(f"\n找到匹配的字符串!")
print(f"原始字符串: '{candidate}'")
print(f"MD5哈希值: {hash_val}")
print(f"验证: 前缀匹配 - {hash_val.startswith(args.prefix)}")
print(f"验证: 后续数字 - {hash_val[2:].isdigit()}")
print(f"耗时: {time.time() - start_time:.2f}秒")
else:
print("\n未找到匹配的字符串,请尝试增加最大长度或更换字符集")
except KeyboardInterrupt:
print("\n用户中断,终止搜索")
sys.exit(1)
except Exception as e:
print(f"发生错误: {str(e)}")
sys.exit(1)