查看原文
其他

dedecms修改前台用户密码漏洞分析

2018-01-14 reklawetihwx 看雪学院



前言



  • 漏洞公布时间:2018/01/10

  • 影响版本:V5.7SP2正式版(2018-01-09)之前所有版本



漏洞说明



这个漏洞的前提是需要开启用户注册的功能,造成的危害是能够修改前台部分用户的密码,这部分用户是那些没有设置密保问题的用户。前台管理员密码虽然也没有设置密保问题,但是由于dedecms本身的功能即使修改密码也是无法登录的。

 

dedecms重置密码的原理是给重置密码的用户发送一个重置密码的链接。那么在进行重置密码时,修改为其他的用户就能够修改其他用户的密码了,所以本质上来说这是一个越权漏洞。



漏洞分析



假设dedecms已经开启了用户注册的功能,用户重置密码的的URL是为http://localhost/member/resetpassword.php。对应于源码的位置是member/resetpassword.php。


safequestion分析


在member/resetpassword.php中存在三个重置密码的方法,分别是getped、safequestion、getpasswd。而本次的漏洞与safequestion有关。


分析safequestion方法代码:


else if($dopost == "safequestion")

{

    $mid = preg_replace("#[^0-9]#", "", $id);

    $sql = "SELECT safequestion,safeanswer,userid,email FROM #@__member WHERE mid = '$mid'";

    $row = $db->GetOne($sql);

    if(empty($safequestion)) $safequestion = '';

 

    if(empty($safeanswer)) $safeanswer = '';

 

    if($row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer)

    {

        sn($mid, $row['userid'], $row['email'], 'N');

        exit();

    }

    else

    {

        ShowMsg("对不起,您的安全问题或答案回答错误","-1");

        exit();

    }

 

}


其中有几个关键的验证,如empty($safequestion)、empty($safeanswer)、if($row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer)。首先我们需要知道,一个没有设置密保问题的用户,默认的safequestion和safeanswer结果。


mysql> SELECT safequestion,safeanswer,userid,email FROM dede_member WHERE mid='1';

+--------------+------------+--------+-------+

| safequestion | safeanswer | userid | email |

+--------------+------------+--------+-------+

|            0 |            | admin  |       |

+--------------+------------+--------+-------+

1 row in set (0.00 sec)


默认情况下的safequestion为0,safeanswer为空。那么如何通过这个验证呢?如果我们传输的safequestion为0,而empty($safequestion)为True。此时我们需要利用到php的隐式类型转换,即PHP在进行类型比较、类型判断时会自动进行一些类型的转换,如下:

var_dump(empty(0));             // true

var_dump(empty('0'));           // true

var_dump(empty('0.0'));         // false

var_dump(0.0==0);               // true

var_dump('0.0'==0);             // true

var_dump(null=='');             // true


所以我们设置我们输入的safequestion为0.0,safeanswer为空就可以绕过这个验证,此时empty($safeanswer)为True,$row['safequestion'] == $safequestion也为True,而数据中查询出来的safeanswer本身就为NULL,所以我们设置为空,就可以通过验证。之后程序进入sn($mid, $row['userid'], $row['email'], 'N');中。

 

所以这个地方重置的仅仅是那些没有设置密保问题的用户,因为只有这些用户他们的safequestion才是空


sn函数分析


跟踪进入到member/inc/inc_pwd_functions.php:sn()中,

function sn($mid,$userid,$mailto, $send = 'Y')

{

    global $db;

    $tptim= (60*10);

    $dtime = time();

    $sql = "SELECT * FROM #@__pwd_tmp WHERE mid = '$mid'";

    $row = $db->GetOne($sql);

    if(!is_array($row))

    {

        //发送新邮件;

        newmail($mid,$userid,$mailto,'INSERT',$send);

    }

    //10分钟后可以再次发送新验证码;

   elseif($dtime - $tptim > $row['mailtime'])

    {

        newmail($mid,$userid,$mailto,'UPDATE',$send);

    }

    //重新发送新的验证码确认邮件;

    else

    {

        return ShowMsg('对不起,请10分钟后再重新申请', 'login.php');

    }

}


进入到sn()函数之后,会执行SELECT * FROM #@__pwd_tmp WHERE mid = '$mid',此条SQL语句查询的是dede_pwd_tmp,此表存储的就是重置密码的临时KEY。由于此时没有重置密码,所以没有对应此用户的记录。

 

 

进入到第一个判断newmail($mid,$userid,$mailto,'INSERT',$send);中


newmail函数分析


追踪进入到member/inc/inc_pwd_functions.php:newmail()中


function newmail($mid, $userid, $mailto, $type, $send)

{

    global $db,$cfg_adminemail,$cfg_webname,$cfg_basehost,$cfg_memberurl;

    $mailtime = time();

    $randval = random(8);

    $mailtitle = $cfg_webname.":密码修改";

    $mailto = $mailto;

    $headers = "From: ".$cfg_adminemail."\r\nReply-To: $cfg_adminemail";

    $mailbody = "亲爱的".$userid.":\r\n您好!感谢您使用".$cfg_webname."网。\r\n".$cfg_webname."应您的要求,重新设置密码:(注:如果您没有提出申请,请检查您的信息是否泄漏。)\r\n本次临时登陆密码为:".$randval." 请于三天内登陆下面网址确认修改。\r\n".$cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&id=".$mid;

    if($type == 'INSERT')

    {

        $key = md5($randval);

        $sql = "INSERT INTO `#@__pwd_tmp` (`mid` ,`membername` ,`pwd` ,`mailtime`)VALUES ('$mid', '$userid',  '$key', '$mailtime');";

        if($db->ExecuteNoneQuery($sql))

        {

            if($send == 'Y')

            {

                sendmail($mailto,$mailtitle,$mailbody,$headers);

                return ShowMsg('EMAIL修改验证码已经发送到原来的邮箱请查收', 'login.php','','5000');

            } else if ($send == 'N')

            {

                return ShowMsg('稍后跳转到修改页', $cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&id=".$mid."&key=".$randval);

            }

        }

        else

        {

            return ShowMsg('对不起修改失败,请联系管理员', 'login.php');

        }

    }


进入到$type == 'INSERT'中,生成一个临时KEY,$key = md5($randval);,然后插入到数据库中,$sql = "INSERT INTO #@__pwd_tmp (mid ,membername ,pwd ,mailtime)VALUES ('$mid', '$userid',  '$key', '$mailtime');";。


接下来根据参数$send的值判断是将重置密码的链接通过邮箱发送还是直接跳转。这个参数最开始是在$dopost == "safequestion"中设置的,默认值是N,那么就会将对应id的密码返回。

 

 

拿到重置链接直接在浏览器中访问就可以修改此id对应用户的密码了。

 


重置密码


重置密码发送的请求如下:

URL:http://localhost/member/resetpassword.php

POST:dopost=getpasswd&setp=2&id=3&userid=test02&key=K5TrsKQB&pwd=123456&pwdok=123456


其中的pwd和pwdok是我设置的重置密码。

 

此重置密码的请求就会进入到member/resetpassword.php的$dopost == "getpasswd"中

else if($dopost == "getpasswd")

{

    //修改密码

    if(empty($id))

    {

        ShowMsg("对不起,请不要非法提交","login.php");

        exit();

    }

    $mid = preg_replace("#[^0-9]#", "", $id);

    $row = $db->GetOne("SELECT * FROM #@__pwd_tmp WHERE mid = '$mid'");

    if(empty($row))

    {

        ShowMsg("对不起,请不要非法提交","login.php");

        exit();

    }

    if(empty($setp))

    {

        $tptim= (60*60*24*3);

        $dtime = time();

        if($dtime - $tptim > $row['mailtime'])

        {

            $db->executenonequery("DELETE FROM `#@__pwd_tmp` WHERE `md` = '$id';");

            ShowMsg("对不起,临时密码修改期限已过期","login.php");

            exit();

        }

        require_once(dirname(__FILE__)."/templets/resetpassword2.htm");

    }

    elseif($setp == 2)

    {

        if(isset($key)) $pwdtmp = $key;

 

        $sn = md5(trim($pwdtmp));

        if($row['pwd'] == $sn)

        {

            if($pwd != "")

            {

                if($pwd == $pwdok)

                {

                    $pwdok = md5($pwdok);

                    $sql = "DELETE FROM `#@__pwd_tmp` WHERE `mid` = '$id';";

                    $db->executenonequery($sql);

                    $sql = "UPDATE `#@__member` SET `pwd` = '$pwdok' WHERE `mid` = '$id';";

                    if($db->executenonequery($sql))

                    {

                        showmsg('更改密码成功,请牢记新密码', 'login.php');

                        exit;

                    }

                }

            }

            showmsg('对不起,新密码为空或填写不一致', '-1');

            exit;

        }

        showmsg('对不起,临时密码错误', '-1');

        exit;

    }

}


进入之后,会进行$db->GetOne("SELECT * FROM #@__pwd_tmp WHERE mid = '$mid'")查选,因为在重置密码时已经在dede_pwd_tmp表中保存了记录,所以此时存在数据。

 

 

之后根据step的值为2,进入到更改密码的操作中。更改密码之后会进行$sn = md5(trim($pwdtmp));if($row['pwd'] == $sn操作,与数据库的中密码进行校验,校验成功之后,就会执行一下的两条SQL语句:

"DELETE FROM `#@__pwd_tmp` WHERE `mid` = '$id';"            # 删除临时密码表中的数据

"UPDATE `#@__member` SET `pwd` = '$pwdok' WHERE `mid` = '$id';";    # 更新dede_memeber中的密码


至此就完整了整个任意用户密码的重置过程。



POC



这个漏洞的POC也比较的简单,通过safequestion方法重置密码即可。

URL:member/resetpassword.php

POST:dopost=safequestion&safequestion=0.0&safeanswer=&id=用户ID



总结



这个漏洞其实并没有使用什么特殊技巧,仅仅是由于程序在校验的时候不严格。所以这个漏洞也很好修复,将$row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer中的==变为===。


除此之外,我在调试这个漏洞时发现这个safequestion并没有对应到前台的某个操作,所以感觉这个方法一直没有被使用,而这个漏洞刚好利用了这个方法,所以如果不用这个方法的是完全可以删除的,最后就看官方如何修复吧。







本文由看雪SRC小组 reklawetihwx 原创,系看雪web小组征稿系列文章

转载请注明来自看雪社区



热门阅读


点击阅读原文/read,

更多干货等着你~



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存