美文网首页代码审计
PHP代码审计实践——DeDecms V5.7

PHP代码审计实践——DeDecms V5.7

作者: book4yi | 来源:发表于2021-12-24 11:27 被阅读0次

    前言:


    马上就没有工作了,趁找不到工作这段时间潜心学习PHP代审,多多少少给自己增加一丢丢竞争力,刚毕业半年可太难了。

    • 会员中心任意用户密码修改:

    这是dedecms比较出名的一个漏洞,此漏洞位于会员中心——修改密码,这里需要注意的是,dedecms默认是关闭会员中心的,需要在后台开启会员中心。

    漏洞代码分析:/dedecms/member/resetpassword.php

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

    这里可以得知:

    1. 可以通过安全问题验证来修改密码,$dopostuserid用户可控
    2. 将数据库中查询出来的$row['safequestion']$row['safeanswer']进行了弱类型的比较,$safequestion$safeanswer用户可控,这也是最关键的地方

    我们首先尝试先注册一个用户,不设置安全问题,也不填写问题答案:

    执行上述代码的SQL语句查看返回结果:

    也就是说$row['safequestion']= 0,由于这里进行了弱类型比较,可以使$safequestion=00,绕过empty()函数,$row['safeanswer']默认为空,这里不理它就能使$row['safeanswer'] == $safeanswer恒成立。接下来跟踪sn函数:

    /**
     *  查询是否发送过验证码
     *
     * @param     string  $mid  会员ID
     * @param     string  $userid  用户名称
     * @param     string  $mailto  发送邮件地址
     * @param     string  $send  为Y发送邮件,为N不发送邮件默认为Y
     * @return    string
     */
    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');
            }
        }
    

    $type == 'INSERT'时,会在数据表生成该用户的临时密码,其中pwd为8位随机字符串的key通过md5加密后的值。接着由于传入的$send = N,会返回一个修改用户密码的地址,接着跳转:

    尝试构造数据包:

    美滋滋,可以修改任意用户的密码了:

    • 前台文件上传漏洞(CVE-2018-20129):

    漏洞代码分析:dedecms/include/dialog/select_images_post.php

    require_once(dirname(__FILE__)."/config.php");
    require_once(dirname(__FILE__)."/../image.func.php");
    

    继续追踪所包含的PHP文件:common.inc.php,然后定位到如下:

    //转换上传的文件相关的变量及安全处理、并引用前台通用的上传函数
    if($_FILES)
    {
        require_once(DEDEINC.'/uploadsafe.inc.php');
    }
    

    发现在进行文件上传时引入了全局过滤:/include/uploadsafe.inc.php

    //这里强制限定的某些文件类型禁止上传
    $cfg_not_allowall = "php|pl|cgi|asp|aspx|jsp|php3|shtm|shtml";
    
        if(!empty(${$_key.'_name'}) && (preg_match("#\.(".$cfg_not_allowall.")$#i",${$_key.'_name'}) || !preg_match("#\.#", ${$_key.'_name'})) )
        {
            if(!defined('DEDEADMIN'))
            {
                exit('Not Admin Upload filetype not allow !');
            }
        }
    
        $imtypes = array
        (
            "image/pjpeg", "image/jpeg", "image/gif", "image/png", 
            "image/xpng", "image/wbmp", "image/bmp"
        );
    
        if(in_array(strtolower(trim(${$_key.'_type'})), $imtypes))
        {
            $image_dd = @getimagesize($$_key);
            if (!is_array($image_dd))
            {
                exit('Upload filetype not allow !');
            }
        }
    }
    

    通过上述代码可以知道,上传的文件名不得有php等敏感后缀名,并且对Content-Type进行了限制,似乎并没有什么问题,但是在dedecms/include/dialog/select_images_post.php又进行了一次过滤:

    $imgfile_name = trim(preg_replace("#[ \r\n\t\*\%\\\/\?><\|\":]{1,}#", '', $imgfile_name));
    
    if(!preg_match("#\.(".$cfg_imgtype.")#i", $imgfile_name)) # $cfg_imgtype = 'jpg|gif|png';
    {
        ShowMsg("你所上传的图片类型不在许可列表,请更改系统对扩展名限定的配置!", "-1");
        exit();
    }
    $nowtme = time();
    $sparr = Array("image/pjpeg", "image/jpeg", "image/gif", "image/png", "image/xpng", "image/wbmp");
    $imgfile_type = strtolower(trim($imgfile_type));
    if(!in_array($imgfile_type, $sparr))
    {
        ShowMsg("上传的图片格式错误,请使用JPEG、GIF、PNG、WBMP格式的其中一种!","-1");
        exit();
    }
    

    第二次过滤将\r\n\t*%\?等特殊字符进行了置空处理,那么问题来了,假如上传文件名为test.jpg.p?hpContent-Type:image/jpeg就能绕过上述不能包含php等敏感后缀名的限制

    php文件上传成功且成功执行:

    • URL重定向:

    漏洞代码分析:/dedecms/plus/download.php

    else if($open==1)
    {
        //更新下载次数
        $id = isset($id) && is_numeric($id) ? $id : 0;
        $link = base64_decode(urldecode($link));
        if ( !$link )
        {
            ShowMsg('无效地址','javascript:;');
            exit;
        }
        $row = $dsql->GetOne("SELECT * FROM `#@__softconfig` ");
        $sites = explode("\n", $row['sites']);
        $allowed = array();
        foreach($sites as $site)
        {
            $site = explode('|', $site);
            $domain = parse_url(trim($site[0]));
            $allowed[] = $domain['host'];
        }
        if ( !in_array($linkinfo['host'], $allowed) )
        {
            ShowMsg('非下载地址,禁止访问','javascript:;');
            exit;
        }
        
        header("location:$link");
        exit();
    }
    

    首先注意到header("location:$link");,然后发现$link = base64_decode(urldecode($link));,对$link进行了url解码并进行了base64解码,根据解码的内容进行跳转:

    最开始还担心if ( !in_array($linkinfo['host'], $allowed) )此处判断会提前中断,但是全局搜索$linkinfo并没有相关的信息,这就很多余了。

    • 后台 GetShell:
    • 方式一

    漏洞代码分析:dedecms/dede/sys_verifies.php

    else if ($action == 'getfiles')
    {
        if(!isset($refiles))
        {
            ShowMsg("你没进行任何操作!","sys_verifies.php");
            exit();
        }
        $cacheFiles = DEDEDATA.'/modifytmp.inc';
        $fp = fopen($cacheFiles, 'w');
        fwrite($fp, '<'.'?php'."\r\n");
        fwrite($fp, '$tmpdir = "'.$tmpdir.'";'."\r\n");
        $dirs = array();
        $i = -1;
        $adminDir = preg_replace("#(.*)[\/\\\\]#", "", dirname(__FILE__));
        foreach($refiles as $filename)
        {
            $filename = substr($filename,3,strlen($filename)-3);
            if(preg_match("#^dede/#i", $filename)) 
            {
                $curdir = GetDirName( preg_replace("#^dede/#i", $adminDir.'/', $filename) );
            } else {
                $curdir = GetDirName($filename);
            }
            if( !isset($dirs[$curdir]) ) 
            {
                $dirs[$curdir] = TestIsFileDir($curdir);
            }
            $i++;
            fwrite($fp, '$files['.$i.'] = "'.$filename.'";'."\r\n");//"'\'\"
        }
        fwrite($fp, '$fileConut = '.$i.';'."\r\n");
        fwrite($fp, '?'.'>');
        fclose($fp);
    

    从代码中可以看出,将$refiles数组的内容写入到了modifytmp.inc文件中,而$refiles是用户可控的,这时可以考虑是否存在写入恶意代码到inc文件中的可能性,关键代码:

    fwrite($fp, '$files['.$i.'] = "'.$filename.'";'."\r\n");
    

    然后我们再观察common.inc.php文件中是否存在全局过滤:

        foreach(Array('_GET','_POST','_COOKIE') as $_request)
        {
            foreach($$_request as $_k => $_v)
            {
                if($_k == 'nvarname') ${$_k} = $_v;
                else ${$_k} = _RunMagicQuotes($_v);
            }
        }
    
    function _RunMagicQuotes(&$svar)
    {
        if(!get_magic_quotes_gpc())
        {
            if( is_array($svar) )
            {
                foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
            }
            else
            {
                if( strlen($svar)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#',$svar) )
                {
                  exit('Request var not allow!');
                }
                $svar = addslashes($svar);
            }
        }
        return $svar;
    }
    
    if (!defined('DEDEREQUEST'))
    {
        //检查和注册外部提交的变量   (2011.8.10 修改登录时相关过滤)
        function CheckRequest(&$val) {
            if (is_array($val)) {
                foreach ($val as $_k=>$_v) {
                    if($_k == 'nvarname') continue;
                    CheckRequest($_k);
                    CheckRequest($val[$_k]);
                }
            } else
            {
                if( strlen($val)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#',$val)  )
                {
                    exit('Request var not allow!');
                }
            }
        }
    
        //var_dump($_REQUEST);exit;
        CheckRequest($_REQUEST);
        CheckRequest($_COOKIE);
    
    

    从代码中可以得知,对用户的输入进行了全局过滤,对所有的参数值都执行了addslashes(),那么如果想要注入恶意代码就需要解决两个问题:

    1. 绕过addslashes
    2. 闭合双引号

    接着我们注意到这段代码:$filename = substr($filename,3,strlen($filename)-3);
    灰常奇怪,它去掉了输入的前三个字符,如果输入:\",经过addslashes函数后变成:\\\",然后去掉前三个字符,刚好能闭合双引号,注入恶意代码:

    然后就去找包含了modifytmp.inc的php文件即可实现getshell:

    else if($action=='down')
    {
        $cacheFiles = DEDEDATA.'/modifytmp.inc';
        require_once($cacheFiles);
    
    • 方式二

    漏洞代码分析:dedecms/dede/stepselect_main.php
    首先注意到包含了这个文件:require_once(DEDEINC.'/enums.func.php');查看相关代码,又发现了if(!file_exists(DEDEDATA.'/enums/system.php')) WriteEnumsCache();
    /enums/system.php文件默认不存在,追踪WriteEnumsCache()函数:

    function WriteEnumsCache($egroup='')
    {
        global $dsql;
        $egroups = array();
        if($egroup=='') {
            $dsql->SetQuery("SELECT egroup FROM `#@__sys_enum` GROUP BY egroup ");
        }
        else {
            $dsql->SetQuery("SELECT egroup FROM `#@__sys_enum` WHERE egroup='$egroup' GROUP BY egroup ");
        }
        $dsql->Execute('enum');
        while($nrow = $dsql->GetArray('enum')) {
            $egroups[] = $nrow['egroup'];
        }
        foreach($egroups as $egroup)
        {
            $cachefile = DEDEDATA.'/enums/'.$egroup.'.php';
            $fp = fopen($cachefile,'w');
            fwrite($fp,'<'."?php\r\nglobal \$em_{$egroup}s;\r\n\$em_{$egroup}s = array();\r\n");
            $dsql->SetQuery("SELECT ename,evalue,issign FROM `#@__sys_enum` WHERE egroup='$egroup' ORDER BY disorder ASC, evalue ASC ");
            $dsql->Execute('enum');
            $issign = -1;
            $tenum = false; //三级联动标识
            while($nrow = $dsql->GetArray('enum'))
            {
                fwrite($fp,"\$em_{$egroup}s['{$nrow['evalue']}'] = '{$nrow['ename']}';\r\n");
                if($issign==-1) $issign = $nrow['issign'];
                if($nrow['issign']==2) $tenum = true;
            }
            if ($tenum) $dsql->ExecuteNoneQuery("UPDATE `#@__stepselect` SET `issign`=2 WHERE egroup='$egroup'; ");
            fwrite($fp,'?'.'>');
            fclose($fp);
            if(empty($issign)) WriteEnumsJs($egroup);
        }
        return '成功更新所有枚举缓存!';
    }
    

    $egroup默认为空,从代码中发现会从sql查询中得到的egroup直接写入到文件中,并没有任何的过滤处理:fwrite($fp,'<'."?php\r\nglobal \$em_{$egroup}s;\r\n\$em_{$egroup}s = array();\r\n");这时考虑是否可以插入恶意的代码到数据表中,然后发现如下代码:

    else if($action=='addenum_save')
    {
        if(empty($ename) || empty($egroup)) 
        {
             Showmsg("类别名称或组名称不能为空!","-1");
             exit();
        }
        if($issign == 1 || $topvalue == 0)
        {
            $enames = explode(',', $ename);
            foreach($enames as $ename)
            {
                $arr = $dsql->GetOne("SELECT * FROM `#@__sys_enum` WHERE egroup='$egroup' AND (evalue MOD 500)=0 ORDER BY disorder DESC ");
                if(!is_array($arr)) $disorder = $evalue = ($issign==1 ? 1 : 500);
                else $disorder = $evalue = $arr['disorder'] + ($issign==1 ? 1 : 500);
                    
                $dsql->ExecuteNoneQuery("INSERT INTO `#@__sys_enum`(`ename`,`evalue`,`egroup`,`disorder`,`issign`) 
                                        VALUES('$ename','$evalue','$egroup','$disorder','$issign'); "); 
            }
            WriteEnumsCache($egroup);                                                          
            ShowMsg("成功添加枚举分类!".$dsql->GetError(), $ENV_GOBACK_URL);
            exit();
        }
    

    $ename$egroup用户可控,当$issign=1时可以将$egroup插入到数据表中,然后执行WriteEnumsCache($egroup);,这里要考虑到"?php\r\nglobal \$em_{$egroup}s;\r\n\$em_{$egroup}s = array();\r\n",故构造egroup=;phpinfo();$,即<?php\r\nglobal $em_;phpinfo();$s;\r\n\$em_{$egroup}s = array();\r\n

    • 方式三(CVE-2020-18917)

    漏洞代码分析:dedecms/plus/search.php

    //查找栏目信息
    if(empty($typeid))
    {
        $typenameCacheFile = DEDEDATA.'/cache/typename.inc';
        if(!file_exists($typenameCacheFile) || filemtime($typenameCacheFile) < time()-(3600*24) )
        {
            $fp = fopen(DEDEDATA.'/cache/typename.inc', 'w');
            fwrite($fp, "<"."?php\r\n");
            $dsql->SetQuery("Select id,typename,channeltype From `#@__arctype`");
            $dsql->Execute();
            while($row = $dsql->GetArray())
            {
                fwrite($fp, "\$typeArr[{$row['id']}] = '{$row['typename']}';\r\n");
            }
            fwrite($fp, '?'.'>');
            fclose($fp);
        }
    

    跟方式二一样的问题,直接将SQL语句查询出来的结果写入到typename.inc文件中,并没有进行任何的过滤处理,那么想办法将精心构造payload插入到数据表中,全局搜索:__arctype
    发现 dedecms/dede/catalog_add.php允许插入数据到相关数据表中

        //创建目录
        if($ispart != 2)
        {
            $true_typedir = str_replace("{cmspath}", $cfg_cmspath, $typedir);
            $true_typedir = preg_replace("#\/{1,}#", "/", $true_typedir);
            if(!CreateDir($true_typedir))
            {
                ShowMsg("创建目录 {$true_typedir} 失败,请检查你的路径是否存在问题!","-1");
                exit();
            }
        }
        
        $in_query = "INSERT INTO `#@__arctype`(reid,topid,sortrank,typename,typedir,isdefault,defaultname,issend,channeltype,
        tempindex,templist,temparticle,modname,namerule,namerule2,
        ispart,corank,description,keywords,seotitle,moresite,siteurl,sitepath,ishidden,`cross`,`crossid`,`content`,`smalltypes`)
        VALUES('$reid','$topid','$sortrank','$typename','$typedir','$isdefault','$defaultname','$issend','$channeltype',
        '$tempindex','$templist','$temparticle','default','$namerule','$namerule2',
        '$ispart','$corank','$description','$keywords','$seotitle','$moresite','$siteurl','$sitepath','$ishidden','$cross','$crossid','$content','$smalltypes')";
    
        if(!$dsql->ExecuteNoneQuery($in_query))
        {
            ShowMsg("保存目录数据时失败,请检查你的输入资料是否存在问题!","-1");
            exit();
        }
    

    未完待续.......

    参考如下:


    Dedecms 最新版漏洞收集并复现学习
    通过DedeCMS学习php代码审计

    相关文章

      网友评论

        本文标题:PHP代码审计实践——DeDecms V5.7

        本文链接:https://www.haomeiwen.com/subject/indfqrtx.html