资讯动态

PHP伪随机数漏洞实战:利用php_mt_seed破解CTF抽奖题

发布时间:2026/8/8 23:55:49 来源:尧图企业网站定制
1. 项目概述与核心思路最近在复盘一场CTF比赛遇到一道典型的Web抽奖题核心逻辑是用mt_rand()生成中奖号码。这种题目在CTF和实际渗透测试中都很常见它暴露了PHP伪随机数生成器PRNG的一个经典弱点确定性。简单来说只要种子seed确定mt_rand()生成的随机数序列就完全固定。题目通常不会直接给你种子而是让你通过有限的输出结果去反向推导这正是php_mt_seed工具的用武之地。这个项目就是带你完整走一遍利用php_mt_seed破解此类漏洞的实战流程从环境搭建、原理分析到脚本编写最终拿到flag。无论你是CTF新手想入门Web安全还是开发人员想了解如何安全地使用随机数这篇实战记录都能给你清晰的指引。2. 伪随机数漏洞原理深度解析2.1 什么是伪随机数在计算机中真正的随机数很难获得通常需要依赖物理熵源如鼠标移动、键盘敲击时间差。因此绝大多数编程语言提供的“随机数”函数生成的都是伪随机数。伪随机数由一个确定的算法伪随机数生成器PRNG根据一个初始值种子计算出一系列数字。这些数字在统计上看起来是随机的分布均匀、相互独立但只要算法和种子相同生成的序列就一模一样。PHP中mt_rand()函数使用的是一种名为“梅森旋转”Mersenne Twister的算法它速度快、周期长在PHP 7.1之前是默认的随机数生成器。其基本使用流程是mt_srand($seed)播种用给定的整数$seed初始化生成器的内部状态。mt_rand()根据当前内部状态计算并输出下一个伪随机数同时更新内部状态以准备生成下一个数。关键漏洞点如果攻击者能够获取到mt_rand()生成的部分数值并且知道这些数值在序列中的大致位置就有可能利用工具推算出当初使用的种子。一旦种子被破解整个未来的甚至过去的随机数序列就完全暴露了。2.2 为什么mt_rand()在CTF中成为高频考点这源于它的几个特性默认播种在PHP 7.1.0之前如果调用mt_rand()之前没有显式调用mt_srand()PHP会自动以一个“随机”的种子进行播种。这个“随机”种子在旧版本中可能强度不足例如在某些环境下使用了微秒时间。即使在较新版本中如果开发者使用了弱种子如时间戳也容易被预测。算法公开梅森旋转算法是公开的。这意味着其内部状态转换的数学公式是已知的为逆向推导种子提供了理论基础。输出可观测在Web题目中随机数常常直接或间接如经过简单运算后展示给用户例如作为抽奖号码、验证码、临时文件名的一部分。这为攻击者提供了必要的“已知条件”。工具成熟php_mt_seed这个工具就是专门为爆破mt_rand()种子而生的它针对算法进行了高度优化爆破速度极快通常在几秒到几分钟内就能完成使得攻击在实战中非常可行。注意PHP 7.1.0 之后mt_srand()和mt_rand()的实现有变化引入了模偏置消除等改进但核心的确定性特性未变。php_mt_seed工具也同步支持了新版本的算法。在实际解题或测试时务必确认目标PHP版本。2.3 攻击场景还原一个典型的抽奖题逻辑假设题目代码如下简化版?php session_start(); if (!isset($_SESSION[seed])) { // 使用当前时间秒级作为种子这是一个常见但不安全的做法 $_SESSION[seed] time(); mt_srand($_SESSION[seed]); // 生成10个“随机”的中奖号码存入SESSION $_SESSION[winning_numbers] []; for ($i 0; $i 10; $i) { $_SESSION[winning_numbers][] mt_rand(1, 10000); } } $user_guess (int)$_POST[guess]; $lottery_round (int)$_SESSION[round]; // 假设当前是第N轮抽奖 if ($user_guess $_SESSION[winning_numbers][$lottery_round]) { echo 恭喜你中奖了Flag: . $flag; } else { echo 很遗憾没中奖。正确号码是: . $_SESSION[winning_numbers][$lottery_round]; // 或者只告诉你对错不告诉你正确号码 } ?漏洞利用链种子基于time()这是一个在较小时间窗口内可预测的值。中奖号码序列由该种子完全确定。如果题目在某一轮告诉了你正确的号码例如告诉你没中奖并展示了正确号码你就获得了一个mt_rand()的输出值及其在序列中的位置第$lottery_round个。利用php_mt_seed结合这个已知输出和位置可以爆破出种子$_SESSION[seed]。知道种子后你就可以完整推算出所有10个中奖号码从而在后续轮次中“百分百中奖”。3. 核心工具 php_mt_seed 详解与实战准备3.1 php_mt_seed 是什么php_mt_seed是一个用C语言编写的高效命令行工具唯一的功能就是根据一个或多个已知的mt_rand()输出值暴力破解出生成这些值所使用的原始种子seed。它的作者充分利用了梅森旋转算法的数学特性将暴力搜索的空间进行了大量剪枝和优化使得搜索速度比普通的暴力破解快好几个数量级。3.2 获取与编译工具源码通常托管在开源代码仓库。你需要一个Linux或类Unix环境Windows可通过WSL或Cygwin进行编译。# 1. 克隆或下载源码 git clone https://github.com/openwall/php_mt_seed.git cd php_mt_seed # 2. 编译 make编译成功后当前目录会生成一个名为php_mt_seed的可执行文件。你可以通过./php_mt_seed来运行它。3.3 工具基本用法与参数理解运行./php_mt_seed不加任何参数会显示帮助信息。其核心用法是./php_mt_seed value1 [value2 ...]value1,value2... 是你已知的mt_rand()输出值。但直接这样用通常不行因为你需要告诉工具这些值对应的mt_rand()调用参数和顺序。关键在于如何描述一个“已知条件” 工具接受一种特定的参数格式来定义每个已知的随机数./php_mt_seed min1-max1 min2-max2 ...或者更常见的当你知道确切的输出值时./php_mt_seed exact_value1 exact_value2 ...但这里的exact_value需要根据mt_rand()的调用方式转换而来。mt_rand()的两种调用形式mt_rand(): 返回 0 到mt_getrandmax()通常为2147483647之间的随机整数。mt_rand($min, $max): 返回$min到$max含之间的随机整数。对于第二种形式最常用工具需要的输入是该随机数在 0 到 2147483647 之间的原始映射值而不是你看到的$min到$max之间的值。因为mt_rand($min, $max)内部是先生成一个0~2147483647的大数再通过取模缩放映射到指定范围。转换公式 已知$observed mt_rand($min, $max)求其在0~2147483647范围内的可能原始值区间[low, high]。 这个映射不是一对一的因为取模操作会导致多个原始值映射到同一个输出值。公式推导如下设原始大随机数为$raw(0 $raw 2147483647)。$observed $min ($raw % ($max - $min 1))。 那么$raw % ($max - $min 1) $observed - $min。 这意味着$raw可以表示为$raw k * ($max - $min 1) ($observed - $min)其中k是某个非负整数。 同时$raw必须满足 0 $raw 2147483647。因此工具需要的输入参数是满足上述等式的$raw的最小可能值(low)和最大可能值(high)。我们可以通过计算得到这个区间然后以low-high的格式传递给php_mt_seed。实操心得绝大多数CTF题目都使用mt_rand($min, $max)的形式。手工计算这个区间比较麻烦所以我们通常写一个小的PHP脚本来完成这个转换这也是后面完整攻击脚本的一部分。4. 完整攻击流程与脚本实现我们假设一个实战场景题目是一个多轮抽奖每轮你需要提交一个猜测数字。如果你猜错它会告诉你该轮的正确数字是什么这很关键。你的目标是预测下一轮的中奖数字。4.1 第一步信息收集与观察确定PHP版本通过题目错误信息、响应头等判断。这关系到使用php_mt_seed的哪个算法模式工具通常自动处理但心里要有数。假设目标是PHP 7.4.33。分析题目逻辑通过源代码审计如果给、参数尝试或逻辑推理确定随机数如何被使用例如mt_rand(1000, 9999)生成一个4位数作为中奖码。种子何时初始化通常在一次会话开始时可能基于时间、SESSION ID等。我们能看到哪些随机数输出以及它们分别是第几个生成的例如第一轮我们猜错被告知正确数字是2345那么这个2345就是第一个mt_rand()的输出。4.2 第二步编写辅助脚本进行数据转换与爆破我们需要一个脚本它能够根据我们观察到的mt_rand($min, $max)输出值自动计算出对应的原始值区间。调用php_mt_seed进行爆破。利用爆破出的种子重现随机数序列预测未来的值。以下是完整的攻击脚本exploit.php?php /** * PHP mt_rand 预测攻击脚本 * 用于CTF或安全测试中根据已知的mt_rand输出破解种子并预测后续值。 * 使用前需在相同系统架构下编译好 php_mt_seed 工具。 */ // 配置区域 $php_mt_seed_path ./php_mt_seed; // php_mt_seed 工具路径 $php_version ‘7.4’; // 目标PHP版本影响mt_rand内部实现细节 // 已知条件你观测到的 mt_rand($min, $max) 的输出值及其位置和参数 // 格式 [‘index’ 序号 (从0开始), ‘value’ 观测值, ‘min’ 参数$min, ‘max’ 参数$max] $known_values [ [‘index’ 0, ‘value’ 2345, ‘min’ 1000, ‘max’ 9999], // 例如第一轮的正确号码 // 如果你有多个观测值可以继续添加增加爆破的准确性和速度 // [‘index’ 1, ‘value’ 6789, ‘min’ 1000, ‘max’ 9999], ]; // 需要预测的未来随机数的参数通常和已知条件相同 $predict_min 1000; $predict_max 9999; $predict_count 5; // 需要预测后续多少个数字 /** * 将观测到的 mt_rand($min, $max) 值转换为 php_mt_seed 所需的参数格式 * param int $observed 观测到的值 * param int $min mt_rand 参数 $min * param int $max mt_rand 参数 $max * return array [low, high] 原始值区间 */ function observed_to_range($observed, $min, $max) { $range_size $max - $min 1; $offset $observed - $min; $mt_rand_max mt_getrandmax(); // 通常是 2147483647 $low $offset; $high $low; $k 1; // 找出所有满足 $raw k * $range_size $offset 且在 0 ~ mt_rand_max 之间的值 while (true) { $candidate $k * $range_size $offset; if ($candidate $mt_rand_max) { break; } $high $candidate; $k; } // 理论上 low 可能为0但根据公式第一个k是0此时 candidate offset即 low offset // 为了安全我们计算一下最小的有效k $low $offset; // 当k0时 // 但需要确保 low 0这显然成立 // 同时如果 offset mt_rand_max理论上不可能因为observed在min-max之间。 // 一个更严谨的写法 $low $offset; $high $low; for ($k 1; ; $k) { $candidate $k * $range_size $offset; if ($candidate $mt_rand_max) { break; } $high $candidate; } // 检查k0的情况是否有效candidateoffset 肯定在范围内 // 所以 low 就是 offset high 是最后一个有效的candidate return [$low, $high]; } /** * 构建 php_mt_seed 命令行参数 * param array $known_values 已知条件数组 * return string 命令行参数字符串 */ function build_seed_args($known_values) { $args []; // php_mt_seed 需要按顺序为每一个生成的随机数指定条件。 // 我们需要根据已知值的‘index’为序列中每一个位置设置条件。 // 对于未知的位置我们用 “0-2147483647” 表示任何值都可能即无约束。 // 首先找出最大的索引以确定序列长度。 $max_index max(array_column($known_values, ‘index’)); for ($i 0; $i $max_index; $i) { $found false; foreach ($known_values as $kv) { if ($kv[‘index’] $i) { list($low, $high) observed_to_range($kv[‘value’], $kv[‘min’], $kv[‘max’]); $args[] “$low-$high”; $found true; break; } } if (!$found) { // 该位置没有观测值使用全范围约束 $args[] “0-“ . mt_getrandmax(); } } return implode(‘ ‘, $args); } /** * 执行 php_mt_seed 并解析结果 * param string $args 命令行参数 * return array 成功返回种子数组失败返回空数组 */ function run_php_mt_seed($args) { global $php_mt_seed_path; $command “$php_mt_seed_path $args 21”; echo “[*] 执行命令: $command\n”; exec($command, $output, $return_var); $seeds []; foreach ($output as $line) { if (preg_match(‘/^Found seed: (\d)/’, $line, $matches)) { $seeds[] intval($matches[1]); } } return $seeds; } /** * 验证种子并预测未来值 * param int $seed 候选种子 * param array $known_values 已知条件用于验证 * param int $predict_min 预测范围最小值 * param int $predict_max 预测范围最大值 * param int $predict_count 预测数量 * return array|false 如果种子有效返回预测数组否则返回false */ function verify_and_predict($seed, $known_values, $predict_min, $predict_max, $predict_count) { mt_srand($seed); // 验证已知值 foreach ($known_values as $kv) { // 生成到该索引之前的随机数 for ($i 0; $i $kv[‘index’]; $i) { mt_rand($kv[‘min’], $kv[‘max’]); // 我们不关心值但需要消耗状态 } $generated mt_rand($kv[‘min’], $kv[‘max’]); if ($generated ! $kv[‘value’]) { return false; // 种子不匹配 } // 重置生成器状态为下一个验证或预测做准备不行状态已经前进。 // 更好的做法为每个验证重新播种。但这里我们采用另一种方式记录所有生成的值。 } // 如果验证通过重新播种并生成完整的预测序列 mt_srand($seed); $max_index max(array_column($known_values, ‘index’)); $all_numbers []; for ($i 0; $i $max_index $predict_count; $i) { $all_numbers[] mt_rand($predict_min, $predict_max); } // 提取预测部分已知值之后的部分 $predictions array_slice($all_numbers, $max_index 1, $predict_count); return $predictions; } // —————— 主程序开始 —————— echo “[*] PHP mt_rand 预测攻击脚本\n”; echo “[*] 已知条件: \n”; foreach ($known_values as $kv) { echo “ - 第 {$kv[‘index’]} 个随机数范围[{$kv[‘min’]}, {$kv[‘max’]}]观测值: {$kv[‘value’]}\n”; } // 1. 构建爆破参数 $seed_args build_seed_args($known_values); echo “[*] 生成的 php_mt_seed 参数: $seed_args\n”; // 2. 运行 php_mt_seed 爆破种子 echo “[*] 开始爆破种子…\n”; $possible_seeds run_php_mt_seed($seed_args); if (empty($possible_seeds)) { echo “[!] 未找到可能的种子。请检查已知条件是否正确或尝试增加已知随机数的数量。\n”; exit(1); } echo “[*] 共找到 “ . count($possible_seeds) . ” 个候选种子。\n”; // 3. 验证种子并预测 $valid_predictions []; foreach ($possible_seeds as $seed) { echo “[*] 验证种子: $seed\n”; $predictions verify_and_predict($seed, $known_values, $predict_min, $predict_max, $predict_count); if ($predictions ! false) { echo “[] 种子 $seed 验证成功\n”; $valid_predictions[$seed] $predictions; // 通常第一个成功的种子就是正确的但我们可以继续验证其他种子如果有 // break; // 如果确定唯一性可以break } } if (empty($valid_predictions)) { echo “[!] 所有候选种子验证失败。可能原因\n”; echo “ 1. 已知条件有误如索引位置不对。\n”; echo “ 2. PHP版本差异导致算法不一致。\n”; echo “ 3. 题目使用了 mt_rand() 的无参数形式但脚本按有参数处理。\n”; exit(1); } // 4. 输出预测结果 echo “\n[] 攻击成功预测结果如下\n”; foreach ($valid_predictions as $seed $predictions) { echo “ 使用种子: $seed\n”; echo “ 后续 “ . count($predictions) . ” 个预测值 (“ . $predict_min . “~” . $predict_max . “):\n”; foreach ($predictions as $idx $value) { echo “ 第 ” . ($idx 1) . “ 个: $value\n”; } } ?4.3 第三步执行攻击与获取Flag环境准备将编译好的php_mt_seed可执行文件与上面的exploit.php脚本放在同一目录。确保php_mt_seed有执行权限 (chmod x php_mt_seed)。修改配置根据题目实际情况修改脚本顶部的配置变量$known_values: 填入你从题目中收集到的信息。index从0开始计数表示这是第几个mt_rand()调用。$predict_min,$predict_max: 题目中mt_rand()使用的范围。$predict_count: 你需要预测的未来随机数的个数。运行脚本在命令行中执行php exploit.php。分析结果脚本会输出爆破出的可能种子以及对应的未来随机数预测值。提交预测在题目界面提交预测出的下一个数字。如果题目设计正确你将成功“中奖”获得flag。5. 常见问题、排查技巧与防御建议5.1 攻击过程中可能遇到的问题及解决思路问题现象可能原因排查与解决思路php_mt_seed运行后无输出或报错1. 工具未编译成功。2. 参数格式错误。3. 已知条件约束过强无解。1. 检查编译过程是否有警告/错误。尝试运行./php_mt_seed 1-1测试基本功能。2. 仔细检查observed_to_range函数逻辑用已知种子生成几个随机数再用脚本反向计算区间看是否能覆盖原始种子。3. 尝试减少已知条件如只用一个观测值或检查观测值的索引是否正确。找到多个候选种子且验证后预测结果不一致1. 已知条件不足导致多个种子都满足当前约束。2. 观测值的索引位置判断错误。1.这是最常见的情况。你需要收集更多的已知随机数输出。每增加一个正确的已知条件就能排除大量错误种子。通常2-3个连续的随机数输出就足以唯一确定种子。2. 仔细审计题目逻辑确认mt_rand()被调用的确切顺序和次数。注意是否有其他地方如初始化、其他功能也调用了mt_rand()消耗了状态。脚本验证种子失败所有候选种子都被排除1. PHP版本不匹配。2.mt_rand()调用方式判断错误有无参数。3. 题目对随机数进行了额外处理如加密、哈希。1. 确认目标PHP版本。php_mt_seed默认支持旧算法PHP 7.1对于PHP 7.1需要使用-v参数指定模式如-v 2。在我们的脚本中需要在构建命令时加入版本参数。2. 如果题目使用mt_rand()无参数形式那么观测值本身就是0~2147483647的大数无需转换。修改observed_to_range函数直接返回[$observed, $observed]。3. 观察题目输出看是否是原始随机数。如果是处理过的需要逆向处理过程还原出原始的mt_rand()输出值。爆破速度非常慢已知条件约束太弱如只给了一个范围很大的随机数。增加已知条件是最有效的方法。如果无法获得更多输出可以尝试利用种子的可能范围如种子是时间戳可以估算一个时间区间进行爆破。php_mt_seed也支持指定种子搜索范围-s start end。5.2 给开发者的安全建议如何避免此类漏洞绝对不要使用mt_rand()/rand()用于安全目的包括生成密码重置令牌、会话标识符、抽奖中奖号码、验证码等任何涉及安全或公平性的场景。使用密码学安全的随机数生成器CSPRNGPHP提供了random_int()和random_bytes()函数。它们旨在生成密码学上安全的随机数可以抵御此类预测攻击。替换方案将mt_rand($min, $max)直接替换为random_int($min, $max)。确保种子不可预测如果因历史原因必须使用mt_rand()那么种子的熵源必须足够强大。使用random_bytes()生成种子然后通过mt_srand()注入。// 不安全的种子 mt_srand(time()); mt_srand(12345); // 相对安全的种子但仍不推荐用于安全场景 $seed hexdec(bin2hex(random_bytes(4))); // 生成一个32位的随机整数作为种子 mt_srand($seed);意识到随机数的“状态”是全局的mt_rand()的状态是进程内全局的。如果一个应用的不同部分甚至同一个请求的不同地方都调用它会相互消耗随机数序列这可能意外暴露序列信息。使用CSPRNG可以避免这个问题因为每次调用都是独立的。5.3 CTF 进阶技巧状态泄露与偏移有时题目不会直接给你连续的随机数输出。比如它可能用第一个随机数生成A用第五个随机数生成B。你需要通过源代码分析确定这两个输出在序列中的精确偏移量例如相差4个mt_rand()调用并将这个信息体现在$known_values的index中。处理mt_rand()无参数形式如果题目使用mt_rand()脚本需要调整。php_mt_seed可以直接接受exact_value作为参数因为输出就是原始的大随机数。利用错误信息有时题目在猜错时不会显示正确值但可能会因为比较操作产生不同的响应时间时序攻击或细微的报错信息差异这可能间接泄露信息。但这属于更复杂的侧信道攻击范畴。结合其他漏洞伪随机数预测常与反序列化、文件包含等漏洞结合。例如预测出的随机数可能作为临时文件名的一部分进而实现文件上传覆盖或包含。这个漏洞的利用过程清晰地展示了“确定性”在安全上下文中的危险性。作为攻击者理解系统如何工作是其弱点作为开发者理解这些工具的局限性并选择更安全的替代方案是构建稳健系统的第一步。在CTF赛场上这类题目是经典的“送分题”但在真实世界里它提醒我们看似无害的“随机”函数也可能成为整个系统安全链条中最脆弱的一环。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价