[GWCTF 2019]枯燥的抽奖

1

知识点:

1.substr(substring子字符串)

->意味提取一部分字符串

举例: substr($str, 0, 10);表示在str字符串中,从0处开始截取,一次截取10位

2.伪随机

  1. 算法的本质是“确定性函数”

PHP 的 mt_rand() 使用的是 Mersenne Twister(梅森旋转算法)

  • 数学本质:它是一个复杂的递归公式。每一个新产生的数字 $X_{n}$ 都是基于前一个状态 $X_{n-1}$ 通过位运算(移位、与、异或)计算出来的。
  • 输入决定输出:该算法接受一个初始值,即 Seed(种子)。一旦 Seed 确定,算法生成的整个数字序列就完全固定了。
  1. 状态机的内部机理

计算机没有“灵魂”去随机想一个数字,它运行的是一个状态机

  • 种子 (Seed):初始化状态机的内部状态向量(Mersenne Twister 拥有一个 624 个 32 位整数的状态阵列)。
  • 序列生成:每次你调用 mt_rand(),算法会对状态阵列执行一次“旋转”(Twist)变换,并提取出一个数字输出。
  • 不可逆性 vs 可预测性:虽然通过单个输出很难直接反推状态阵列,但如果你拥有足够多的连续输出(样本),就能通过数学方法联立方程组,反解出状态阵列,从而预测后续所有输出。
  1. 为什么能“作弊”

由于 seed 的范围通常是 32 位整数($2^{32}$,约 42.9 亿),这在现代计算能力面前非常小。

  • 暴力枚举:工具如 php_mt_seed 会利用 CPU 的并行指令集,在几秒钟内模拟 42.9 亿次初始化的过程。
  • 验证逻辑:工具把每一个可能的 seed 都跑一遍,看它生成的第 1 到第 10 个随机数是否与你页面上显示的字符索引一致。
  • 结果唯一性:因为算法是确定的,只要前 10 个数对上了,这个 seed 就是唯一的,那么第 11 到第 20 个数也必然完全一致。

抽奖猜数,直接看源码没有啥思路

抓包发现有个check界面,他说不让我们看那我们就去看看

image-20260206155614836

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
6zgZ4qvPR0

<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
if($_POST['num']===$str){x
echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>没抽中哦,再试试吧</p>";
}
}
show_source("check.php");

原本的思路是是不抽奖找其他漏洞的,但是我想学一下寻找种子的方法

我以前一直以为种子是随机生成的,怎么会找到呢,现在觉得当时想的太简单了

已知:有一个长度为20的字符串,但是他只显示给我们10个

而我们竟然可以通过这10个字符串去寻找剩下的兄弟姐妹:

逻辑就是:

1.收集已有样本(10个字符)

2.拿样本去让工具php_mt_seed找到能生成包括了这十个的种子

3.拿到种子,也就拿到了伪随机计算公式,跑脚本去

首先你需要用到一个工具,我是在kali里使用的

下载指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 下载工具压缩包
wget https://www.openwall.com/php_mt_seed/php_mt_seed-4.0.tar.gz

# 2. 解压
tar -zxvf php_mt_seed-4.0.tar.gz

# 3. 进入目录
cd php_mt_seed-4.0

# 4. 编译(Kali 自带 gcc)
make

# 5. 现在运行命令(就在这个目录下跑)
./php_mt_seed 32 32 0 61 25 25 0 61 6 6 0 61 61 61 0 61 30 30 0 61 16 16 0 61 21 21 0 61 51 51 0 61 53 53 0 61 26 26 0 61

已经把使用的命令放在上面了,但是我们还不懂这是咋使用的:

已得:十个字符为6zgZ4qvPR0

abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ

我们先看到字母表的第32个刚好是6,说明就是去表里面找到一一对应的位置序号

6->32

32 32 0 61 的意思是:第一个生成的随机数确实是 32,且当时 mt_rand 的范围设定是 0 到 61。

使用方法:

1
./php_mt_seed "四个为一组的数字集合,用空格隔开"

得到

1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿kali)-[~/Desktop/php_mt_seed-4.0]
└─$ ./php_mt_seed 32 32 0 61 25 25 0 61 6 6 0 61 61 61 0 61 30 30 0 61 16 16 0 61 21 21 0 61 51 51 0 61 53 53 0 61 26 26 0 61
Pattern: EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62
Version: 3.0.7 to 5.2.0
Found 0, trying 0xfc000000 - 0xffffffff, speed 685.2 Mseeds/s
Version: 5.2.1+
Found 0, trying 0x02000000 - 0x03ffffff, speed 47.3 Mseeds/s
seed = 0x027f2981 = 41888129 (PHP 7.1.0+) ->得到咱们的php版本号以及种子41888129
Found 1, trying 0xfe000000 - 0xffffffff, speed 45.2 Mseeds/s
Found 1

拿到种子写脚本

源码

1
2
3
4
5
6
7
8
mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);

所以我们要按照源码的逻辑跟他生成一样的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// 使用你跑出来的种子
mt_srand(41888129);

$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; //复刻字母表
$str = ''; //准备好放num的容器

// 循环 20 次生成完整字符串
for ($i = 0; $i < 20; $i++){
$str .= substr($str_long1, mt_rand(0, 61), 1);
}

echo "完整的 num 值为: " . $str;
?>



输出:
完整的 num 值为: 6zgZ4qvPR0b636lsfQ73

抓包传入即可

image-20260206162416824