浅谈PHP伪随机数

536 阅读4分钟

我正在参与掘金创作者训练营第5期,点击了解活动详情

什么是php伪随机数

首先,让我们先观察一下这两个函数,来看一下函数分别如何作用。

mt_scrand()
mt_rand()  

mt_scrand(seed)这个函数的意思就是分发seed种子,种子有了后,可以依靠mt_rand()生成随机数。

例如我们写一个脚本

<?php  
mt_srand(55555);    
echo mt_rand()."<br/>";
?> 
//输出277249736

如果我们多输入几次呢

<?php  
mt_srand(12345);    
echo mt_rand()."<br/>";
echo mt_rand()."<br/>";
echo mt_rand()."<br/>";
echo mt_rand()."<br/>";
?>
//输出277249736<br/>230135029<br/>1872932720<br/>1536240435<br/>

大家可以发现,这个随机数是不是不是这么随机,这就是伪随机数的漏洞,存在可预测性

为了方便理解,我将其认为是线性的给大家画个图演示一下

图片.png (画的不好不要在意)x就是我们所说的种子,知道了种子和伪随机数就可以推y了

通过以上的举例说明,我们便可以推出以下的结论:

1.当我们知道种子数,便可以推出伪随机数列。

2.当我们知道伪随机数列,便可以推出种子。

当我们知道这些后,大家可以想一下,应该如何破解伪随机数呢?我们接下来来介绍一个工具

php_mt_seed

该工具专门用来爆破种子,贴一个官方网站:www.openwall.com/php_mt_seed…

官方简介:This is a PHP mt_rand() seed cracker. In the most trivial invocation

mode, it finds possible seeds given the very first mt_rand() output

after possible seeding with mt_srand(). With advanced invocation modes,

is also able to match multiple, non-first, and/or inexact mt_rand()

outputs to possible seed values.

用法:

将文件夹放进kali,进入该文件目录里,首次使用先输入make,然后

./php_mt_seed 所要爆破的随机数

我们举几个例子

example1

先写一个脚本

<?php
function user_password() {


    return mt_rand();
    
    }
    
    echo user_password(), "\n";
    
    echo user_password(), "\n";
    
    echo user_password(), "\n";
    
    echo user_password(), "\n";


?>

我们随机分配密码,运行后

266584478
1966871117
983911992
151017562

如果我们只知道第一个密码,也就是266584478,那我们是否能利用工具将后面三个密码给推出来呢,我们用上面提到的工具对第一个密码进行爆破

ex1.PNG

跑出来种子276062199(这里注意一点php版本是要匹配的,否则随机出来的数是不同的)

接下来我们写个脚本,已知种子跑随机数

<?php
mt_srand(276062199);


for($i=0;$i<4;$i++){


echo mt_rand()." ";


}
?>

运行出来

266584478 1966871117 983911992 151017562 

可以看到我们推出了后三个密码,当然这是最简单的例子,接下来我们通过网络安全比赛中的题目来进阶一下,加深理解。

example2

GWCTF2019(枯燥的抽奖)

猜字符串游戏(大小写字母+数字),猜中全部20位得flag+送去非洲,你不小心偷看到了一部分是:


NssQvWQ6a1

不知所云,看看有啥提示

看源码发现有个check.php,直接访问一下

NssQvWQ6a1
<?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");

大体看了一下,我认为是一个随机的种子分配一个值,我们就是要找到种子数

首先用到一个工具php_mt_seed-4.0

我们要先转换,把数据转能工具能解读的状态

这里直接贴一个脚本

str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='NssQvWQ6a1'
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):  
    for j in range(len(str1)):
        if str2[i] == str1[j]:
            res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
            break
print(res)

结果

49 49 0 61 18 18 0 61 18 18 0 61 52 52 0 61 21 21 0 61 58 58 0 61 52 52 0 61 32 32 0 61 0 0 0 61 27 27 0 61 

kali先进入文件夹,然后make

time ./php_mt_seed 第一个随机数 

运行后可以得到一下结果:

Found 0, trying 0x36000000 - 0x37ffffff, speed 45.9 Mseeds/s 
seed = 0x37d845d8 = 936920536 (PHP 7.1.0+)

之后用脚本转字符

需要注意的一点是,php版本要对的上才能跑出来正确的字符,很关键的一步

<?php
mt_srand(936920536);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);       
}
echo "<p id='p1'>".$str."</p>";
?> 

[Running] php "c:\Users\XINO\Desktop\1 (2).php"

<p id='p1'>NssQvWQ6a1O0pXe8nMxb</p>

[Done] exited with code=0 in 0.135 seconds

得到结果后我们直接上交就可以出现flag

flag{c8be10cb-9d75-4cdb-85f8-d0a8e329863a} 

example3

MRCTF2020(Ezaudit)

经过渗透测试发现网站存在源码泄露可以在 www.zip 的路径下下载源码。

<?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???

简单看一眼,需要我们传用户名密码私钥的参数,username= 'crispr' 已经固定,我们无法再改,密码可控,我们可以尝试万能密码,重点在怎样用公钥求私钥

  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;
  }

根据私钥加密算法会发现随机数的生成用了mt_rand函数,该地方存在一个伪随机数的问题

用固定脚本去跑一下

str1 ='KVQP0LdJKRaV3n9D'
str2 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result =''




length = str(len(str2)-1)
for i in range(0,len(str1)):
    for j in range(0,len(str2)):
        if str1[i] ==  str2[j]:
            result += str(j) + ' ' +str(j) + ' ' + '0' + ' ' + length + ' '
            break




print(result)

用工具php_mt_seed跑出来

seed = 0x69cf57fb = 1775196155 (PHP 5.2.1 to 7.0.x; HHVM)

之后我们使用脚本进行加密

<?php
mt_srand(1775196155);
//公钥
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;
}
//私钥
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

在登录界面填上对应数据,即可解出这一题。

总结

挺多CTF题都是从CVE提取出来的关键代码出的,本文讲的便是17年php爆出了一个可以根据随机种子可以被逆推的漏洞。有兴趣的小伙伴可以去了解一下,本文就不过多叙述了。