1. 从'爆个锤子'到'伪随机数'的认知升级
做 CTF 题目最怕什么?不是题目难,而是思路一开始就错了。我刚开始做 CTFshow Web25 这道题时,就犯了个低级错误——看到代码里有个 md5($flag),然后截取前 8 位转十进制作为种子,我下意识以为种子就是 ctfshow{ 这八个字符的 MD5 值。结果折腾了半天,发现完全不对路。
后来仔细看代码才明白,人家是先对整个 flag 进行 MD5 加密,然后取前 8 位十六进制,再转换成十进制作为种子。这个区别可大了去了,就像你以为密码是'123456',结果人家用的是'123456'的 SHA256 值,完全是两码事。
这道题的核心逻辑其实挺有意思的。代码里有个关键判断:if((!$rand)),意思是只有当 $rand 为 0 时,才会执行后面的 token 验证逻辑。而 $rand 的计算方式是 intval($r)-intval(mt_rand())。所以最简单的思路就是让 $r 等于 mt_rand() 的值,这样两者相减就是 0。
但问题来了,我们不知道 mt_rand() 会生成什么值啊。这时候有个小技巧:我们可以先传 ?r=0,这样 $rand 就等于 -mt_rand(),服务器会把这个负的随机数回显给我们。比如我测试时得到了 -646081337,那么 mt_rand() 就是 646081337。
你以为这就完了?更麻烦的还在后面。代码里验证 token 的逻辑是:$_COOKIE['token']==(mt_rand()+mt_rand())。注意这里的 mt_rand()+mt_rand() 可不是同一个随机数加两次,而是连续调用两次 mt_rand() 得到两个不同的值再相加。
我刚开始也犯糊涂,以为 token 就是 2*mt_rand(),写了个小测试就发现不对:
<?php mt_srand(123456); $rand1 = mt_rand(); $rand2 = mt_rand(); echo $rand1,"\n",$rand2; ?>
同样的种子,第一次和第二次生成的随机数完全不同。所以我们需要的是第二次和第三次 mt_rand() 的和,因为第一次已经被我们用 ?r=0 的方式'消耗'掉了。
2. php_mt_seed:伪随机数的'时光倒流'机器
知道了随机数值,怎么反推种子呢?理论上可以写脚本爆破,但 32 位的种子空间有 40 多亿种可能,纯暴力破解太慢了。这时候就该 php_mt_seed 这个神器出场了。
我在实际复现时发现,直接运行脚本可能因环境差异导致结果偏差。更稳妥的方式是结合本地 PHP 环境验证生成的随机数序列,确保种子匹配后再进行全量爆破。通常只需要前三个随机数即可锁定种子范围,大大缩短计算时间。最后将算出的种子代入原代码逻辑,即可成功计算出正确的 token 值,拿到 Flag。

