*文章原创作者:路过你身边topcoder,本文 未经许可禁止转载

序言

PHP的mt_rand函数作为一个随机数生成工具在程序中被广泛使用,但是大家都忽略了一个事实,mt_rand生成的随机数不是一个真正的随机数,而是一个伪随机数,不能应用于生成安全令牌、核心加解密key等等,所以很多知名程序都出现过对mt_rand函数的错误使用而导致的安全问题。php_mt_seed是一个破解mt_rand函数种子的工具,对它应用场景的深刻理解和应用能极大的提升漏洞发现的可能和利用的成功率。本文将详细介绍PHP mt_rand函数的安全问题及php_mt_seed应用场景。

PHP mt_rand函数简介

PHP的mt_rand函数作为一个随机数生成工具在程序中被广泛使用,该函数用了 Mersenne Twister 算法的特性作为随机数发生器,它产生随机数值的平均速度比 libc 提供的 rand() 快四倍。mt_rand函数有两个可选参数 min 和 max,如果没有提供可选参数,mt_rand函数将返回返回 0 到 mt_getrandmax() 之间的伪随机数。例如想要 5 到 15(包括 5 和 15)之间的随机数,用 mt_rand(5, 15)。

常用的使用方式如下:

<?php

echo mt_rand() . "\n";

echo mt_rand() . "\n";

echo mt_rand(5, 15);

?>

以上程序的输出结果如下:

1604716014

1478613278

6

伪随机数

伪随机数是用确定性的算法计算出来的随机数序列,它并不真正的随机,但具有类似于随机数的统计特征,如均匀性、独立性等。在计算伪随机数时,若使用的初值(种子)不变,那么伪随机数的数序也不变。伪随机数可以用计算机大量生成,在模拟研究中为了提高模拟效率,一般采用伪随机数代替真正的随机数。模拟中使用的一般是循环周期极长并能通过随机数检验的伪随机数,以保证计算结果的随机性。伪随机数的生成方法有线性同余法、单向散列函数法、密码法等。

PHP mt_rand引起的安全问题

mt_rand就是一个伪随机数生成函数,它由可确定的函数,通过一个种子产生的伪随机数。这意味着:如果知道了种子,或者已经产生的随机数,都可能获得接下来随机数序列的信息(可预测性)。

简单假设一下 mt_rand()内部生成随机数的函数为: rand = seed+(i*10) 其中 seed 是随机数种子, i 是第几次调用这个随机数函数。当我们同时知道 i 和 rand 两个值的时候,就能很容易的算出seed的值来。比如 rand=21 , i=2 代入函数 21=seed+(2*10) 得到 seed=1 。是不是很简单,当我们拿到seed之后,就能计算出当 i 为任意值时候的 rand 的值了。

我们已经知道随机数的生成是依赖特定的函数,上面曾经假设为 rand = seed+(i*10) 。对于这样一个简单的函数,我们当然可以直接计算出一个解来,但 mt_rand实际使用的函数可是相当复杂且无法逆运算的。有效的破解方法其实是穷举所有的种子并根据种子生成随机数序列再跟已知的随机数序列做比对来验证种子是否正确。php_mt_seed就是这么一个工具,它的速度非常快。它可以根据单次mt_rand()的输出结果直接爆破出可能的种子,当然也可以爆破类似mt_rand(1,100)这样限定了MIN MAX输出的种子。

最后要提一下php官网manual的一个坑,mt_rand中文版和英文版的介绍是不一样的,可以看到英文版多了一块黄色的 Caution 警告:

捕获.PNG

很多国内开发者估计都是看的中文版的介绍而在程序中使用了mt_rand()来生成安全令牌、核心加解密key等等导致严重的安全问题。

这里我们还要注意一点,每一次mt_rand被调用都会根据seed和当前调用的次数i来计算出一个伪随机数,而且seed是自动播种的(自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 给随机数发生器播种 ,因为现在是由系统自动完成的)。那么问题就来了,到底系统自动完成播种是在什么时候,如果每次调用mt_rand()都会自动播种那么破解seed也就没意义了。关于这一点manual并没有给出详细信息。通过对源代码的分析可以看到每次调用mt_rand()都会先检查是否已经播种。如果已经播种就直接产生随机数,否则调用php_mt_srand来播种。也就是说每个php cgi进程期间,只有第一次调用mt_rand()会自动播种。接下来都会根据这个第一次播种的种子来生成随机数。而php的几种运行模式中除了CGI(每个请求启动一个cgi进程,请求结束后关闭。每次都要重新读取php.ini 环境变量等导致效率低下,现在用的应该不多了)以外,基本都是一个进程处理完请求之后standby等待下一个,处理多个请求之后才会回收(超时也会回收)。总的来说,就是php的自动播种发生在php cgi进程中第一次调用mt_rand()的时候。跟访问的页面无关,只要是同一个进程处理的请求,都会共享同一个最初自动播种的种子。

下面我们来看一下mt_rand的克星php_mt_srand。

php_mt_seed简介及使用说明

php_mt_seed是一个破解mt_rand函数seed的工具,在最简单的调用模式下,它能通过mt_rand第一次输出的值寻找mt_rand的seed,在更高级的模式中它能匹配不是第一次输出的和不明确具体输出的情况。

mt_rand的算法从PHP 3.0.6开始就一直在变化,php_mt_seed 4.0 支持以下几个大的版本: PHP 3.0.7 to 5.2.0,PHP 5.2.1 to 7.0.x, and PHP 7.1.0+

php_mt_seed基于命令行运行,命令行可以使用1,2,4或者更多的参数。这些参数需要详细说明mt_rand()的输出。

一个参数

当只有一个参数的时候,这个参数代表mt_rand第一次输出的值。

两个参数

当有两个参数的时候,他们代表mt_rand第一次输出应该位于什么区间内。

第一个参数为最小值,第二个参数为最大值。

四个参数(高级模式)

前两个参数表示mt_rand第一次输出的区间,后两个参数表示mt_rand输出的区间。

多于五个参数(高级模式)

每四个参数一组,但是最后一组可以是1,2或4个参数。每一组引用对应的输出。

php_mt_seed应用场景

直接使用mt_rand生成的随机数

假设下面的代码为用户密码的随机生成代码:

<?php

function user_password() {

return mt_rand();

}

echo user_password(), "\n";

echo user_password(), "\n";

echo user_password(), "\n";

运行后我们可以得到三个用户的密码

QQ截图20181214214913.png

假设我们现在得到了第一个用户的密码:1412203388

通过这个密码我们可以猜测出后面两个用户的密码。

下面我们运行php_mt_seed找出seed,命令如下:

./php_mt_seed.exe 1412203388

运行截图如下:

QQ截图20181214214935.png

这里我用于测试的服务器的PHP版本为5.4.45,那么seed就可能是2078089285,下面写一段PHP代码来测试一下。

<?php

mt_srand(2078089285);//手工播种

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

echo mt_rand()." ";

}

运行结果如下:

QQ截图20181214215800.png

完全解密了其它两个用户的密码。

使用经过转换后的mt_rand随机序列

下面是我们更常见到的生成随机数的代码:

<?php

function user_password($length = 10) {

$allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';

$len = strlen($allowable_characters) - 1;

$pass = '';

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

$pass .= $allowable_characters[mt_rand(0, $len)];

}

return $pass;

}

mt_srand(time());

echo user_password(), "\n";

echo user_password(), "\n";

echo user_password(), "\n";

?>

运行后我们可以得到三个用户的密码:

QQ截图20181214220413.png

假设我们现在得到了第一个用户的密码:paJHuuKv3H

我们要写一个程序,先是把字母还原成为生成的随机数,然后在拼接成php_mt_seed需要的参数。代码如下:

<?php

$pass_now = "paJHuuKv3H";

$allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';

$len = strlen($allowable_characters) - 1;

for($j = 0; $j < strlen($pass_now); $j++)

{

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

if($pass_now[$j] == $allowable_characters[$i])

{

echo "$i $i 0 56 ";

break;

}

}

}

运行结果如下:

QQ截图20181214220835.png

执行如下命令:

./php_mt_seed.exe 14 14 0 56 0 0 0 56 33 33 0 56 32 32 0 56 19 19 0 56 19 19 0 56 34 34 0 56 20 20 0 56 50 50 0 56 32 32 0 56

运行结果如下:

QQ截图20181214221155.png

得到seed为1544796235,写解密代码如下:

<?php

function user_password($length = 10) {

$allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';

$len = strlen($allowable_characters) - 1;

$pass = '';

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

$pass .= $allowable_characters[mt_rand(0, $len)];

}

return $pass;

}

mt_srand(1544796235);

echo user_password(), "\n";

echo user_password(), "\n";

echo user_password(), "\n";

运行截图如下:

QQ截图20181214221557.png

完全解密了其它两个用户的密码。

下面列出一个实际的例子:

Discuz X3.3 authkey生成算法的安全性漏洞

这里只做简要介绍,如需查看详细内容请看参考文献3。

Discuz官方于2017年8月1号发布最新版X3.4版本,在最新版本中修复了多个安全问题。

用户在初次安装软件时,系统会自动生成一个authkey写入全局配置文件和数据库,之后安装文件会被删除。该authkey用于对普通用户的cookie进行加密等密码学操作,但是由于生成算法过于简单,可以利用公开信息进行本地爆破。

Discuz_X3.3_SC_UTF8uploadinstallindex.php中authkey的生成方法如下:

$authkey = substr(md5($_SERVER['SERVER_ADDR'].$_SERVER['HTTP_USER_AGENT'].$dbhost.$dbuser.$dbpw.$dbname.$username.$password.$pconnect.substr($timestamp, 0, 6)), 8, 6).random(10);

可以看出authkey主要由两部分组成:

MD5的一部分(前6位) + random生成的10位

跟入random函数:

QQ截图20181214222347.png

由于字符生成集合是固定的,且没有重复字符,那么函数中每一次生成hash都唯一对应了chars数组中的一个位置,而且是使用同一个seed生成的。

在之后的代码中使用了同样的random函数:

$_config['cookie']['cookiepre'] = random(4).'_';

Cookie的前四个字节是已知的,并且使用了同样的random函数,那么思路很明显:

通过已知的4位,算出random使用的种子,进而得到authkey后10位。那剩下的就需要搞定前6位,根据其生成算法,只好选择爆破的方式,由于数量太大,就一定要选择一个本地爆破的方式(即使用到authkey而且加密后的结果是已知的)。

在调用authcode函数很多的地方都可以进行校验,在这里使用找回密码链接中的id和sign参数。

sign生成的方法如下:

function dsign($str, $length = 16){

return substr(md5($str.getglobal('config/security/authkey')), 0, ($length ? max(8, $length) : 16));

}

爆破authkey 的流程:

1.通过cookie前缀爆破随机数的seed。使用php_mt_seed工具。

2.用seed生成random(10),得到所有可能的authkey后缀。

3.给自己的账号发送一封找回密码邮件,取出找回密码链接。

4.用生成的后缀爆破前6位,范围是0×000000-0xffffff,和找回密码url拼接后做MD5求出sign。

5.将求出的sign和找回密码链接中的sign对比,相等即停止,获取当前的authkey。

总结

说了这么多,那到底随机数怎么不安全了呢?其实函数本身没有问题,官方也明确提示了生成的随机数不应用于安全加密用途(虽然中文版本manual没写)。问题在于开发者并没有意识到这并不是一个真随机数 。我们已经知道,通过已知的随机数序列可以爆破出种子。也就是说,只要任意页面中存在输出随机数或者其衍生值(可逆推随机值),那么其他任意页面的随机数将不再是“随机数”。常见的输出随机数的例子比如验证码,随机文件名等等。常见的随机数用于安全验证的比如找回密码校验值,比如加密key等等。PHP随机数的应用范围很广,很多知名的程序也出现过很多严重的问题,但是,目前这个方面还是没有得到足够的重视,一定还有很多很多地方存在着类似的漏洞等待着我们去发现。

参考文献

[1]http://www.php.net/manual/zh/function.mt-rand.php

[2]http://www.php.cn/php-weizijiaocheng-380106.html

[3]https://www.seebug.org/vuldb/ssvid-96371

[4]https://www.openwall.com/php_mt_seed/

[5]https://github.com/lepiaf/php_mt_seed

[6]https://baike.baidu.com/item/%E4%BC%AA%E9%9A%8F%E6%9C%BA%E6%95%B0/104358?fr=aladdin

*文章原创作者:路过你身边topcoder,本文 未经许可禁止转载

源链接

Hacking more

...