PHP伪随机数预测实战:从CTF彩票题看种子漏洞与代码审计

发布时间:2026/10/19 16:15:24
PHP伪随机数预测实战:从CTF彩票题看种子漏洞与代码审计 灰头土脸折腾了一下午终于把攻防世界lottery这道题完整打下来了。回过头看这题表面是个彩票游戏实际考的是PHP伪随机数预测和业务逻辑漏洞的组合利用难度不算高但很典型。如果你正在刷Web方向的CTF或者对真实业务里抽奖、红包、开箱这类功能的代码审计感兴趣这篇记录应该能给你省下不少绕弯路的时间。先说结论按常规思路这道题的核心突破口就一句话——开奖号码的随机数种子可控。只要抓住这一点后续就是纯粹的算和刷。1. 从一道彩票题说起题目概览与核心考点1.1 题目的外观与交互打开靶机地址映入眼帘的是一个清爽的彩票页面。页面功能很少注册、登录、充值或领取初始金币、选择号码、购买彩票、查看开奖结果。账户初始有一笔小额金币每天或每次访问可能还有签到赠送之类的入口。彩票规则也很简单选几个数字一组全中就是头奖奖池金额直接被加到账户余额里。这种外观很容易让人产生两种错觉一是觉得这是个纯前端的练手题二是觉得需要靠运气反复买才能攒够金币。实际上两个方向都不对。前端页面只是壳真正的逻辑全在后端PHP代码里。做题的第一步就是抛弃运气思维把它当成一个代码审计题来做。1.2 隐藏在三层结构下的真实考点这类Web题基本都有固定的套娃结构lottery也不例外第一层业务交互层。注册、购买、开奖这层是给人看的也是用来埋障眼法的。第二层后端逻辑层。PHP处理请求、调用随机数函数生成号码、比对结果、更新余额。第三层安全考点层。题目真正考察的是伪随机数生成器的可预测性以及种子值是否被错误地设计为用户可控。很多刚接触CTF的同学卡在第二层和第三层之间能看到代码但不知道代码里哪个函数是关键的。这里我给个最直接的检索清单在源码里看到这些函数眼睛就要亮起来mt_srand()mt_rand()rand() / srand()shuffle()内部依赖随机数array_rand()str_shuffle()lottery这道题就是典型的mt_srand()可控。代码大概是这个模式$seed $_COOKIE[seed]; mt_srand($seed); $winNums []; for ($i 0; $i 7; $i) { $winNums[] mt_rand(1, 999); }看到没种子直接取自Cookie。Cookie是用户自己发的等于说开奖号码的生成起点完全由玩家掌控。这不是bug这是题目故意留给你的后门。1.3 这道题在现实业务中的映射有人可能会问这种设计在真实业务里会出现吗讲真类似的代码我在一些中小型公司的活动系统里见过不止一次。抽奖、优惠券、拼团红包、游戏开箱只要业务方图省事用了mt_rand()并且把种子设置成时间戳、用户ID、订单号这类可预测或可控制的量结果就是整个随机体系形同虚设。比如用用户ID做种子攻击者只需要注册一个账户观察几次开奖结果然后本地用相同种子复算就能做到期期命中。再比如用time()做种子爆破窗口只有前后几十秒用工具一样能扫出来。所以这道题不只是CTF里的一个关卡它对应的是一整类真实漏洞。这也是我推荐认真对待它的原因。2. 随机数安全基础为什么伪随机数可以被预测2.1 从MT19937到PHP的mt_rand要理解lottery的考点先得搞清楚mt_rand()背后是什么。PHP的mt_rand()基于梅森旋转算法简称MT19937。这个算法是1997年前后设计的经典伪随机数生成器用途很广泛模拟、采样、游戏等但它有一个决定性特征——它不是密码学安全的随机数生成器。所谓密码学安全通常要求即使攻击者看到了若干输出也无法推断出内部状态或未来输出。而MT19937不满足这一点它的内部状态在数学上是可逆向的输出片段一旦积累足够内部状态就能被重建。这句话翻译成人话就是只要让我看到你连续生成的几个数我就知道接下来你会生成什么。这里插一句密码学层面安全的随机数生成器在Python里是secrets模块、在PHP 7里是random_bytes()或random_int()在Linux内核里就是/dev/urandom。这些源才是做抽奖、令牌、密钥这类敏感场景的正确选择。mt_rand()当玩具可以当安全机制不行。2.2 种子决定一切确定性序列的原理MT19937本身是确定性算法给一个初始种子经过固定的状态初始化过程后面的每一个输出都是状态递推的结果。因此同一个种子永远生成同一个序列。这个性质本身不是漏洞任何确定性伪随机算法都这样。但一旦种子可被外部控制或推导漏洞就出现了。拿lottery举例Cookie里的seed如果设置成12345本地PHP用mt_srand(12345)再连打7次mt_rand(1, 999)得到的结果一定和远端服务器完全相同。原因就是两边跑的是同一套算法、同一个种子、同一个调用序列。所以利用思路就清晰了要么直接改Cookie为自己指定的种子要么从已知输出反推种子。前者在lottery里最常用毕竟种子直接写在Cookie里都不用反推直接管中窥豹。2.3 指定min/max参数时输出发生了什么变化做题时容易被忽略的一个细节是mt_rand(1, 999)和mt_rand()的输出并不存在简单对应关系。你直接调用不带参数的mt_rand()得到的是一个0到最大整数之间的值带$min/$max参数后PHP内部会对原始输出做缩放和取整处理。这个细节对反推种子和本地预测非常致命。网上的很多现成脚本跑出来对不上号八成就是没考虑这个缩放逻辑。我的建议是本地预测时严格复用远端相同版本的PHP和完全相同的函数调用参数。如果要做序列反推先用mt_rand()的原始输出还是用缩放后的值这个得看工具支持的格式。不同PHP版本5.x、7.x、8.x对mt_rand的实现细节有差异一定要先确认远端版本。在攻防世界这个靶场环境里题目用的通常是某个固定PHP版本最简单的确认方式是看响应头、报错信息里的PHP版本号或者直接试本地不同版本的PHP输出和远端接口返回是否一致。2.4 反推种子的常用工具php_mt_seed如果你的目标是反推种子业界最常用的开源工具就是php_mt_seed。它的原理简单说就是把已知的mt_rand()输出片段作为约束条件在种子空间里做快速搜索组合求解。用法也很直观./php_mt_seed 123 456 789把已知的随机数输出作为参数传入工具会枚举可能的种子。需要注意的是已知输出越长反推越快越准。一般拿到连续5个以上输出就能圈定很小的候选集。如果已知的是mt_rand(1, 999)这种带范围的值需要用工具的特定参数模式因为它内部要对输出做逆向缩放还原。种子如果是当前时间戳爆破范围就是前后几分钟速度极快。以前我总觉得反推种子是高深数学用过php_mt_seed之后发现大多数竞赛场景其实就是已知连续输出穷举种子空间。真正难的地方反而是如何让程序吐出几个连续的随机输出。3. 代码审计实战快速定位随机数漏洞点3.1 信息收集与入口分析拿到靶机我习惯的流程是先注册一个账号随便买两期彩票把正常请求的包抓下来看。重点看三样东西Cookie有没有seed、token、user_id这种看起来可以被篡改的字段。接口参数购买彩票的请求里除了号码是否携带其他隐藏参数。响应内容开奖结果是否在响应里明文返回错误信息会不会泄露路径、版本、源码片段。在lottery上抓包最容易发现的就是Cookie里藏着一个seed字段值看起来是一个整数。这时候心里的弦就该绷紧了后端大概率用这个值做了随机种子。3.2 在源码中寻找随机数函数的调用链如果题目直接给了源码压缩包恭喜你难度直线下降。解压后用关键词搜索就能定位核心逻辑。我在实战里常用这条命令快速梭哈grep -rn mt_srand\|mt_rand\|seed --include*.php .如果没给源码就需要结合目录扫描和常见备份文件碰运气比如www.zip、index.php.bak、.git泄露。攻防世界的题目多数会直接提供源码附件所以这步通常不卡人。找到核心文件后重点理解这几件事开奖函数在哪哪个函数负责生成中奖号码。种子来源在哪$_COOKIE、$_GET、$_POST、数据库、还是时间函数。种子是否在每次请求中被重置如果每次请求都重新mt_srand那预测窗口就是单次请求内如果只在会话开始时种一次后续随机数连续预测窗口就是整个会话。购买与开奖的时序是否买了才能看到结果还是结果提前存在可读取的位置。3.3 最常见的高危模式种子从用户可控位置取值审计过程中你会发现lottery的代码逻辑并不复杂危险点就一个mt_srand的参数来自Cookie。这种模式在真实代码里也有对应的糟糕变体种子来自用户ID、用户名、手机号等可猜测字段。种子来自订单号、时间戳且时间戳精度有限。种子写在前端JavaScript里通过隐藏表单字段传回后端。遇到这种模式第一反应应该是我能控制种子吗控制种子之后本地生成的序列和远端同步吗如果两个答案都是能这道题就已经算解了百分之八十。3.4 中奖判定逻辑的额外盲点除了随机数本身我还建议顺手看一下中奖判定的比较逻辑。有些彩票类题目会把中奖判定写成if (in_array($buyNum, $winNums)) { // 算中奖 }这个in_array意味着只要买的号码出现在开奖号码集合里就算中奖不管你买了几个数、顺序如何。这种情况下的漏洞就会从预测全部号码降维成只要预测中一个或多买几个高概率号码覆盖。还有更离谱的写法是只判断数字是否相等不判断类型比如1 1abc这种PHP弱类型比较直接导致构造特殊参数绕过。lottery这题主考点不在比较逻辑上但多看一眼总没坏处万一题目版本有变这可能就是隐藏的快速通道。4. 完整利用过程从预测到稳定刷金币4.1 本地复现环境搭建做这类题必须有本地环境原因是预测依赖完全一致的算法你在浏览器里心算是算不过来的。推荐用Docker快速拉起一个PHP容器版本先选和远端一致的如果不确定就多试几个版本比输出结果。我这次用的是PHP 7.4。把后端核心代码复制一份到本地写一个最简单的调用脚本?php mt_srand(12345); for ($i 0; $i 7; $i) { echo mt_rand(1, 999) . ; }保存为predict.php命令行直接跑php predict.php把输出的7个数字记录下来。然后回到靶机修改Cookie中的seed12345随便买一期彩票比对开奖号码。如果一致说明本地环境和远端完全同步后续工作就是纯体力活了。4.2 一次完整的种子可控验证我在靶机上做了一次完整对比具体操作如下用浏览器插件或者直接curl -b把Cookie的seed字段改为12345。正常点击购买选号码时先随便填提交后拿到开奖号码。本地跑同一个PHP脚本生成12345种子下的前7个mt_rand(1, 999)。两边逐位比对完全吻合。这里有一个细节种子设置后服务端可能已经生成过一些随机数了导致你本地前7个输出对不上远端的第8~14个输出。解决办法是登录后立刻把Cookie里的seed改掉让服务端重新以你指定的种子开始生成序列。如果服务端把种子放在Session里且只在登录时初始化那就在登录之前先准备好Cookie再注册保证第一次开奖用的种子就是你指定的。4.3 自动化购买脚本的编写思路验证通过后手工一注一注买就太蠢了必须上脚本。我的脚本结构很简单核心就是复用本地PHP生成的随机序列不断预测远端下一期号码。import requests import subprocess BASE_URL http://your-target/ PHPSESSID your-session-id SEED 12345 def predict(seed, start_idx, count7): code f?php mt_srand({seed}); for($i0;$i{start_idx count};$i){{$rmt_rand(1,999); if($i{start_idx}) echo $r. ;}} out subprocess.check_output([php, -r, code], textTrue) return list(map(int, out.strip().split())) sess requests.Session() cookies {PHPSESSID: PHPSESSID, seed: str(SEED)} # 假设你已经买了 n 期下一期从 n*7 开始预测 next_nums predict(SEED, 7, 7) resp sess.post( BASE_URL /api/buy, json{numbers: next_nums, action: buy}, cookiescookies ) print(resp.text)这个脚本没有针对特定接口做适配核心想表达的是预测序列是一次性算好的之后每买一期就从序列里切下一段提交。因为种子可控且本地环境一致你可以提前算出几千期开奖号码然后循环提交根本不用等远端实时开奖。一个性能优化的小建议不要每预测一次就调一次PHP子进程启动PHP的开销不小。更高效的做法是用PHP一次性生成一个大数组比如10000个号码写进文件然后Python读文件切片用。我在实际刷的时候就是这么干的速度快很多。4.4 快速拿Flag的路径选择当你拥有稳定中奖能力后金币会指数级增长。这时候有两个思路拿Flag最常规不断累计金币攒够目标价格后购买Flag。页面可能还有个商店或兑换入口金额达到门槛就会出现一个购买Flag的按钮。更快捷审计源码时注意是否有直接读取文件、目录遍历、模板注入等旁路。有的版本里Flag就在可以读文件的位置只是入口被隐藏得很深。有的题故意把Flag价格定得很高逼你不停刷彩票这其实是为了让你感受稳定预测的中奖快感。不过如果追求效率可以顺便看看admin功能、文件下载功能、报错堆栈里会不会泄露敏感路径。我在lottery里走的是第一条常规路径因为题目本身已经把刷彩票这条路铺得很顺畅了换个思路反而浪费时间。5. 实战踩坑与排查记录5.1 本地预测结果和远端对不上这是我最先踩的坑。本地PHP 8.1跑出来的序列和远端返回的开奖号码完全不一致。排查了一圈发现远端用的可能是PHP 5.x或7.x。不同版本之间mt_rand()的算法实现有细微差异尤其是带min/max参数时内部缩放逻辑变化很大。解决思路切换本地PHP版本写个循环脚本把三个常见版本5.6、7.4、8.0的输出都跑一遍跟远端实际开奖结果比对一次哪个版本对上就用哪个。或者看响应头里的X-Powered-By能直接少走弯路。5.2 php_mt_seed反推失败如果题目后续版本把种子改成随机生成不再由Cookie直接控制那就要走反推种子路线。我试着用php_mt_seed反推时一开始总失败后来发现两个原因已知输出值太少反推的候选集太大。至少要拿连续3~5个输出。输入的是mt_rand(1, 999)的缩放值不是mt_rand()的原始输出。需要用工具的特定参数去指定范围否则等价于拿错误数据喂算法。解决思路先确认远端是否有任何接口能把原始mt_rand()输出泄露出来比如某些调试接口、错误页、日志输出。如果没有就从缩放值逆向换算原始值再喂给工具。5.3 请求频率限制与账户金额不足靶机为了防止有人脚本刷金币时常会加简单的频率限制。常见表现是连续请求后接口返回429或者被弹回登录页。解决思路脚本里加上随机延时每次提交前sleep(1~2)别发得跟机关枪似的。账户初始金额不够的时候多注册几个小号利用系统赠送的初始金币分头刷然后再想办法合并。如果平台限制单IP多账号那就要学会把每一注买得足够准尽量压缩失败次数。5.4 其他需要注意的坑会话过期长时间挂着脚本跑Session可能会失效。脚本里要处理登录态过期后的自动重登。Cookie种子被服务端重置有些题目会在开奖后将种子改写为随机值导致后续预测失效。解决办法是每次请求前重新设置Cookie里的seed为你指定的值。中奖门槛有的版本要求7个号码全中才算头奖预测数组长度必须严格对应多一个少一个都会导致提交失败。浮点数精度如果金币数额很大响应里的余额可能用浮点数表示不要用判断用配合容差。我把这些问题整理成一个速查表方便你直接对照现象可能原因排查/解决办法本地序列与远端不一致PHP版本不一致切换本地PHP版本或看响应头确认版本php_mt_seed无结果已知输出太少/未逆向缩放多收集几个输出按工具要求填参数提交后未中奖种子被重置每次请求前重新写入指定seed请求被限流频率过高增加延时、降低请求频率金币不够初始资金少多账号或提高预测成功率6. 从靶场到生产抽奖类业务的安全设计建议6.1 使用密码学安全随机数如果是真实业务里的抽奖、开箱、红包系统mt_rand()从一开始就不该出现。PHP 7及以上直接用random_int()Python用secrets模块Java用SecureRandom。它们同样是随机数但不给攻击者看几个输出就预测后续的机会。有些开发者觉得这些函数用起来麻烦其实差别很小// 不安全的写法 $luckyNumber mt_rand(1, 999); // 安全写法 $luckyNumber random_int(1, 999);改一行代码的事风险等级完全不同。真实项目里如果登录态、优惠券码、密码重置token这类东西还在用rand()那就是把大门钥匙挂在门口。6.2 把随机源与用户输入彻底隔离就算用了安全的随机数生成器也要保证生成过程与用户输入无关。不要出现用户传入种子、后端直接用这种设计。正确姿势是服务端自己在内部生成或存储随机状态不经过任何外部参数。对安全性要求高的场景建议把开奖结果提前生成并落库开奖时只做查询不在请求链路里现算。前端只拿到一个开奖ID后端根据ID返回结果这样即使接口被爆破也无法影响随机结果。6.3 业务侧增加防刷与结果校验机制再完善的随机源也防不住业务逻辑漏洞。真实系统里还应该加上频控同一用户单日购买次数、单IP单位时间请求数。结果校验后端生成结果时记录连续性、金额上下限异常中奖直接告警。幂等同一用户同一期只能购买一份避免并发提交刷奖。审计日志记录每次随机数生成时的上下文出了问题能回溯。这些措施在CTF题目里不常见但真实业务里必须有。你追求的是可复现、可验证、不可篡改而不是看起来很随机。我个人做过一个很类似的业务系统当时为了临时活动赶进度用了mt_rand()和当前时间戳做种子后来被测试同学用爆破工具在几分钟内打穿了。从那之后我形成了一条铁律凡是和钱、权限、身份相关的随机数一律按密钥标准对待。做lottery这道题的时候这种感觉尤其强烈——它用最小的篇幅把伪随机数带来的连锁影响演示得明明白白。最后分享一个小经验刷题时一定要把审计源码优先级提到最高很多时候题目已经把答案写在注释里了。lottery这题如果当初直接抓包改Cookie而不看代码可能也能靠运气蒙对但绝不可能稳定中奖。先读代码再写脚本这对CTF和实际工作都是最有性价比的流程。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询