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

前言:


马上就没有工作了,趁找不到工作这段时间潜心学习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代码审计

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容