1. 先别急着爆,先把随机数链路看明白
做 CTF Web 题最容易踩的坑,不是算不动,而是一上来就把题意读歪了。我最开始看 Web25 时,盯着 md5($flag) 和前 8 位转十进制这个点,默认种子来自 ctfshow{ 这 8 个字符的 MD5。后面才发现,实际是先对整个 flag 做 MD5,再取前 8 位十六进制转成十进制作为种子。差一个范围,思路就完全跑偏了。
题目的核心判断也不复杂:if((!$rand)) 只有在 $rand 为 0 时才会走后面的 token 校验。$rand 的计算是 intval($r)-intval(mt_rand()),所以只要能让 r 和 mt_rand() 的结果相等,这一步就能过。
麻烦在于,我们不知道服务器那次 mt_rand() 会吐什么值。这里有个很实用的办法:先传 ?r=0,这样 $rand 就变成了 -mt_rand(),服务器会把这个负数回显出来。比如回显 -646081337,就能直接反推出这次 mt_rand() 的值是 646081337。
2. token 不是一次随机数,而是连续两次
接下来别被 $_COOKIE['token']==(mt_rand()+mt_rand()) 这行带偏。这里不是同一个随机数加两次,而是连续调用两次 mt_rand(),拿到两个不同的值再相加。
我一开始也顺手把它当成 2*mt_rand() 了,后来用简单测试确认不对:
<?php mt_srand(123456); $rand1 = mt_rand(); $rand2 = mt_rand(); echo $rand1,"\n",$rand2; ?>
同一个种子下,连续两次 mt_rand() 的结果并不相同。也就是说,前面通过 ?r=0 消耗掉了一次随机数之后,token 对应的其实是后两次 mt_rand() 的和。
3. php_mt_seed 的用法,正好对上这类题
知道随机数值以后,理论上可以自己写脚本反推种子,但 32 位空间太大,硬爆不划算。这里直接用 php_mt_seed 更省事,它本来就是拿来做这种逆向的。
我的做法是把已知的 mt_rand() 输出喂给工具,让它去缩小种子范围。因为题目里已经泄露了一个随机数结果,php_mt_seed 能很快把候选种子压到可处理的程度。真正麻烦的不是工具本身,而是先确认你拿到的是第几次随机数——这一步错了,后面全白搭。
这类题的关键点其实就两个:先把 mt_rand() 的调用顺序理清,再用已知输出倒推种子。思路顺的时候,它像是在把随机数倒放;思路不顺的时候,就只是对着一串数字瞎试。
4. 这题值得记住的地方
php_mt_seed 不是万能爆破器,它只在你已经拿到部分随机输出、并且能确认调用顺序时才好用。Web25 这题就是典型场景:先通过回显拿到一次 mt_rand(),再结合后续 token 的生成方式去推后面的随机链路,最后把种子找出来。

