美文网首页代码审计
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

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

  • DedeCMS v5.7 通过文件包含和CSRF的配合利用

    该靶场使用了 DedeCMS v5.7 ,所以将其源码下载,进行代码审计。 文件包含漏洞出现在 /dede/sys...

  • 修改删除织梦dedecms底部版权信息|开拓族

    在DEDECMS V5.7版本更新后,前台网页底部会出现织梦版权信息 “powered by dedecms”,很...

  • 代码审计

    代码审计工具 1、三款自动化代码审计工具教程2、seay源代码审计系统 PHP核心配置详解 注意PHP各个版本中配...

  • 【代码审计】PHP代码审计

    1. 概述 代码审核,是对应用程序源代码进行系统性检查的工作。它的目的是为了找到并且修复应用程序在开发阶段存在的一...

  • 攻防世界(进阶)--WEB--8.Web_php_unseria

    考察点:php代码审计 1.进入场景,得到php代码 2.化简代码,审计 3.写脚本 得到参数:TzorNDoiR...

  • 2019-07-28-php代码审计

    一、PHP代码执行代码审计首先讲一下PHP代码执行漏洞和命令执行漏洞的区别,PHP代码执行指的是将php代码植入到...

  • Php代码审计

    Challenge show_source(__FILE__); $flag="xxxx"; if(isset($...

  • php代码审计

    审计初审 判断审计对象的架构,是否套了开源的框架,若是开源框架,直接利用框架的漏洞进行验证利用;若是原生代码则进行...

  • PHP代码审计

    PHP:include()``include_once()``require()``require_once() ...

网友评论

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

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