【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞

开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day4
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 4 - False Beard代码如下:

class Login {
  public function __construct($user, $pass) {
    $this->loginViaXml($user, $pass);
  }

  public function loginViaXml($user, $pass) {
    if (
      (!strpos($user, '<') || !strpos($user, '>')) &&
      (!strpos($pass, '<') || !strpos($pass, '>'))
    ) {
      $format = '' .
        '';
      $xml = sprintf($format, $user, $pass);
      $xmlElement = new SimpleXMLElement($xml);
      // Perform the actual login.
      $this->login($xmlElement);
    }
  }
}

new Login($_POST['username'], $_POST['password']);

漏洞解析 :
我们看到第11行第12行

      $format = '' .
        '';

程序通过格式化字符串的方式,使用 xml结构存储用户的登录信息。实际上这样很容易造成数据注入。然后 第21行 实例化Login 类:

new Login($_POST['username'], $_POST['password']);

并在 第16行 处调用login 方法进行登陆操作。

 $this->login($xmlElement);

在进行登录操作之前,代码在 第8行第9行 使用strpos函数来防止输入的参数含有 <> 符号:

(!strpos($user, '<') || !strpos($user, '>')) &&
(!strpos($pass, '<') || !strpos($pass, '>'))

猜测开发者应该是考虑到非法字符注入问题。

strpos 函数的定义:

(PHP 4, PHP 5, PHP 7)

功能:

strpos — 查找字符串在另一字符串中第一次出现的位置

定义:
strpos ( string $haystack , mixed $needle [, int $offset = 0 ] ) : int

返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE注释: 字符串位置从 0 开始,不是从 1 开始。

说明:
参数 描述
haystack 在该字符串中进行查找。
needle 如果 needle 不是一个字符串,那么它将被转换为整型并被视为字符的顺序值
offset 如果提供了此参数,搜索会从字符串该字符数的起始位置开始统计。 如果是负数,搜索会从字符串结尾指定字符数开始。

范例


echo strpos("I love php, I love php too!","php");
?>

结果为:7

【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第1张图片
在这道题目中,开发者只考虑到 strpos 函数返回 false 的情况,却忽略了匹配到的字符在首位时会返回 0 的情况,因为false0 的取反均为 true。这样我们就可以在用户名和密码首字符注入<符号,从而注入xml数据。
如果我们构造以下 payload ,观察 strpos 函数的返回结果就会为true:

user=<">
$user='<">;
$pass='';
var_dump(strpos($user,'<')); #在第0位
var_dump(!strpos($user,'<'));#返回true
var_dump(strpos($user,'>'));#在第2位
var_dump(!strpos($user,'>'));#没有就返回false
var_dump(
(!strpos($user, '<') || !strpos($user, '>')) &&   #返回true
(!strpos($pass, '<') || !strpos($pass, '>'))      #返回true
#最终返回true
)
?>

【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第2张图片

很明显是可以注入xml数据的。

实例分析:

本次实例分析,我们 DeDecms V5.7SP2正式版进行分析,本次漏洞是开发者对strpos 函数理解不够,或者说是开发者考虑不周,导致过滤方法可被绕过。由于我们暂时没有在互联网上找到 strpos使用不当导致漏洞的CMS案例,所以这里只能选取一个相似的漏洞进行分析,同样是开发者验证不够周全导致的漏洞。

漏洞POC 本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!

漏洞分析:

我们来看一下本次漏洞的文件,漏洞的触发点在 member/resetpassword.php 文件中,由于对接收的参数safeanswer没有进行严格的类型判断,导致可以使用弱类型比较绕过。具体代码如下(75-95行):

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

}

针对上面的代码做个分析,当 $dopost 等于 safequestion 的时候,通过传入的 $mid 对应的 id 值来查询对应用户的安全问题、安全答案、用户id、电子邮件等信息。跟进到 第10行 ,当我们传入的问题和答案非空,而且等于之前设置的问题和答案,则进入 sn 函数。然而这里使用的是 == 而不是 === 来判断,所以是可以绕过的。假设用户没有设置安全问题答案,那么默认情况下安全问题的值为 0 ,答案的值为 null这里是数据库中的值,即 $row['safequestion']="0" 、 $row['safeanswer']=null )。当没有设置 safequestionsafeanswer 的值时,它们的值均为空字符串。第10行if表达式也就变成了 if('0' == '' && null == '') ,即 if(false && true) ,所以我们只要让表达式 $row['safequestion'] == $safequestiontrue 即可。如下面代码 null == '' 的判断结果:


$a='';
$b;
var_dump($a==$b);
 ?>

【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第3张图片
我们可以利用 php弱类型 了解弱类型的特点,来绕过这里 $row['safequestion'] == $safequestion 的判断:
在这里插入图片描述
通过测试找到了三个的payload,分别是 0.00.0e1 ,这三种类型payload均能使得 $row['safequestion'] == $safequestiontrue

   if($row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer)
    {
        sn($mid, $row['userid'], $row['email'], 'N');
        exit();
    }

即成功进入 sn 函数。跟进 sn 函数,相关代码在 member/inc/inc_pwd_functions.php 文件中,具体代码如下(150-171行):

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 函数内部,会根据midpwd_tmp表中判断是否存在对应的临时密码记录:

    $sql = "SELECT * FROM #@__pwd_tmp WHERE mid = '$mid'";

根据结果确定分支,走向 newmail 函数。

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

假设当前我们第一次进行忘记密码操作,那么此时的 $row 应该为空,所以进入第一个 if(!is_array($row))分支,在newmail函数中执行 INSERT操作,相关操作代码位置在 member/inc/inc_pwd_functions.php 文件中,关键代码如下(82-101行):

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

该代码主要功能是发送邮件至相关邮箱,并且插入一条记录至 #@__pwd_tmp 表中。而恰好漏洞的触发点就在这里,我们看看 第11行 至 第14行 的代码:

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

如果 ($send == 'N') 这个条件为真,通过 ShowMsg 打印出修改密码功能的链接。 第13行 修改密码链接中的 $mid 参数对应的值是用户id,而 $randval是在第一次 insert操作的时候将其 md5加密之后插入到 dede_pwd_tmp 表中,并且在这里已经直接回显给用户。那么这里拼接的url其实是

http://127.0.0.1/member/resetpassword.php?dopost=getpasswd&id=$mid&key=$randval

继续跟进一下 dopost=getpasswd 的操作,相关代码位置在 member/resetpassword.php (96-110行)中:

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

在重置密码的时候判断输入的用户id是否执行过重置密码,如果id为空则退出;如果 $row 不为空,则会执行以下操作内容,相关代码在 member/resetpassword.php (111-122行)中。

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

上面代码会先判断是否超时,如果没有超时,则进入密码修改页面。在密码修改页面会将 $setp 赋值为2
【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第4张图片
由于现在的数据包中 $setp=2 ,因此这部分功能代码实现又回到了 member/resetpassword.php 文件中(123-150行)。

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

上面代码 第6行 判断传入的 $key 是否等于数据库中的 $row['pwd'] ,如果相等就完成重置密码操作,至此也就完成了整个攻击的分析过程。

漏洞利用:

我们分别注册 pz, test 两个账号
由于没有开放注册,我们需要开放注册:

http://10.211.55.5/PHPcode/DedeCMSV57/uploads/dede/login.php?gotopage=%2FPHPcode%2FDedeCMSV57%2Fuploads%2Fdede%2F

进入后台

http://10.211.55.5/PHPcode/DedeCMSV57/uploads/dede/

在左侧的系统–基本参数–左侧栏目–会员设置 在参数设置开启会员功能。是否允许新会员注册等都开启。这是两个比较重要的,不然开启了会员功能没有开启会员注册也是不能用的。
【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第5张图片
【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第6张图片

我们要用pzpztest的密码,登陆pzpz
第一步访问 payload 中的 url

http://10.211.55.5/PHPcode/DedeCMSV57/uploads/member/resetpassword.php?dopost=safequestion&safequestion=0.0&safeanswer=&id=3

【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第7张图片
通过抓包获取到 key 值。
key=cGFPQn2S
【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第8张图片
去掉多余的字符访问修改密码链接

http://10.211.55.5/PHPcode/DedeCMSV57/uploads/member/resetpassword.php?dopost=getpasswd&id=3&key=cGFPQn2S

【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第9张图片
最后就可以修改密码了。

修复建议:

针对上面 DeDecms任意用户密码重置 漏洞,我们只需要使用 === 来代替 == 就行了。因为 === 操作会同时判断左右两边的值和数据类型是否相等,若有一个不等,即返回 false 。具体修复代码如下:

【漏洞练习-Day4】DeDecms V5.7SP2正式版 任意用户密码重置漏洞_第10张图片

结语

再次感谢【红日团队】

你可能感兴趣的:(技术)