发布于2022年11月4日3年前 Dedecms 最新版漏洞收集并复现学习 Dedecms 最新版漏洞收集并复现学习以下漏洞复现均处于最新版dedecms即V5.7 SP2(当然从18年开始就已经没有更新了,应该是没有人维护了)。下载可以直接在官网下载。1. 前台任意用户密码修改漏洞信息无CVE, SSV-97074,提交时间:20180110漏洞成因在用户密码重置功能处,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'); } } 可见在sn函数中将send参数设置了'N',其实就是生成了暂时密码并插入了数据库中,并进行跳转:else if ($send == 'N') { return ShowMsg('稍后跳转到修改页', $cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&id=".$mid."&key=".$randval); } 跳转链接就是修改密码的链接了。复现在找回密码处,点击通过安全问题取回。填写信息并抓包,修改id和userid为想要重置密码的对象,再加上以上分析内容,发包即可得到修改密码url进入该url,修改密码。修复意见改为强类型比较===2. 前台文件上传漏洞漏洞信息CVE-2018-20129, 提交时间:20181221漏洞成因管理员用户前台可以绕过限制上传shell漏洞代码分析漏洞在于用户发布文章上传图片处。处理文件在/include/dialog/select_images_post.php而上传文件存在全局过滤/include/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 !'); } } 可以看到名字中不得有上述字符,且限制了content-type。按道理说直接限制不得存在的字符,似乎没有问题了,可在发布文章文件上传的处理文件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(); } 再次过滤了图片名,并且再次判断如上三种文件类型是否存在其中。这么一次过滤,直接粗暴的将一些特殊字符替换为空,那么我们就可以通过特殊字符绕过上面的全局文件名不能包含php字符的限制,比如文件名为1.jpg.p*hp。复现登录并进入member/article_add.php发布文章,选择下面的富文本编辑器插入图片选择好shell并上传抓包 向如上分析修改文件名与content-type,即可返回shell地址修复意见在最后拼接文件名时再判断一次。3. DeDecms 任意用户登录漏洞信息漏洞编号:SSV-97087,提交时间:20180118漏洞成因dedecms的会员模块的身份认证使用的是客户端session,在Cookie中写入用户ID并且附上ID__ckMd5,用做签名。主页存在逻辑漏洞,导致可以返回指定uid的ID的Md5散列值。原理上可以伪造任意用户登录。漏洞代码分析在/member/index.php中会接收uid和action参数。uid为用户名,进入index.php后会验证Cookie中的用户ID与uid(用户名)并确定用户权限。if($action == '') { include_once(DEDEINC."/channelunit.func.php"); $dpl = new DedeTemplate(); $tplfile = DEDEMEMBER."/space/{$_vars['spacestyle']}/index.htm"; //更新最近访客记录及站点统计记录 $vtime = time(); $last_vtime = GetCookie('last_vtime'); $last_vid = GetCookie('last_vid'); if(empty($last_vtime)) { $last_vtime = 0; } if($vtime - $last_vtime > 3600 || !preg_match('#,'.$uid.',#i', ','.$last_vid.',')) { if($last_vid!='') { $last_vids = explode(',',$last_vid); $i = 0; $last_vid = $uid; foreach($last_vids as $lsid) { if($i>10) { break; } else if($lsid != $uid) { $i++; $last_vid .= ','.$last_vid; } } } else { $last_vid = $uid; } PutCookie('last_vtime', $vtime, 3600*24, '/'); PutCookie('last_vid', $last_vid, 3600*24, '/'); 我们可以看到当uid存在值时就会进入我们现在的代码中,当cookie中的last_vid中不存在值为空时,就会将uid值赋予过去,$last_vid = $uid;,然后PutCookie。那么这么说,我们控制了$uid就相当于可以返回任意值经过服务器处理的md5值。而在接下来会验证用户是否登录。现在我们来看看,dedecms会员认证系统是怎么实现的:/include/memberlogin.class.php //php5构造函数 function __construct($kptime = -1, $cache=FALSE) { global $dsql; if($kptime==-1){ $this->M_KeepTime = 3600 * 24 * 7; }else{ $this->M_KeepTime = $kptime; } $formcache = FALSE; $this->M_ID = $this->GetNum(GetCookie("DedeUserID")); $this->M_LoginTime = GetCookie("DedeLoginTime"); $this->fields = array(); $this->isAdmin = FALSE; if(empty($this->M_ID)) { $this->ResetUser(); }else{ $this->M_ID = intval($this->M_ID); if ($cache) { $this->fields = GetCache($this->memberCache, $this->M_ID); if( empty($this->fields) ) { $this->fields = $dsql->GetOne("Select * From `#@__member` where mid='{$this->M_ID}' "); } else { $formcache = TRUE; } } else { $this->fields = $dsql->GetOne("Select * From `#@__member` where mid='{$this->M_ID}' "); } if(is_array($this->fields)){ #api{{ if(defined('UC_API') && @include_once DEDEROOT.'/uc_client/client.php') { if($data = uc_get_user($this->fields['userid'])) { if(uc_check_avatar($data[0]) && !strstr($this->fields['face'],UC_API)) { $this->fields['face'] = UC_API.'/avatar.php?uid='.$data[0].'&size=middle'; $dsql->ExecuteNoneQuery("UPDATE `#@__member` SET `face`='".$this->fields['face']."' WHERE `mid`='{$this->M_ID}'"); } } } #/aip}} //间隔一小时更新一次用户登录时间 if(time() - $this->M_LoginTime > 3600) { $dsql->ExecuteNoneQuery("update `#@__member` set logintime='".time()."',loginip='".GetIP()."' where mid='".$this->fields['mid']."';"); PutCookie("DedeLoginTime",time(),$this->M_KeepTime); } $this->M_LoginID = $this->fields['userid']; $this->M_MbType = $this->fields['mtype']; $this->M_Money = $this->fields['money']; $this->M_UserName = FormatUsername($this->fields['uname']); $this->M_Scores = $this->fields['scores']; $this->M_Face = $this->fields['face']; $this->M_Rank = $this->fields['rank']; $this->M_Spacesta = $this->fields['spacesta']; $sql = "Select titles From #@__scores where integral<={$this->fields['scores']} order by integral desc"; $scrow = $dsql->GetOne($sql); $this->fields['honor'] = $scrow['titles']; $this->M_Honor = $this->fields['honor']; if($this->fields['matt']==10) $this->isAdmin = TRUE; $this->M_UpTime = $this->fields['uptime']; $this->M_ExpTime = $this->fields['exptime']; $this->M_JoinTime = MyDate('Y-m-d',$this->fields['jointime']); if($this->M_Rank>10 && $this->M_UpTime>0){ $this->M_HasDay = $this->Judgemember(); } if( !$formcache ) { SetCache($this->memberCache, $this->M_ID, $this->fields, 1800); } }else{ $this->ResetUser(); } } } $this->M_ID等于Cookie中的DedUserID,我们继续看看GetCookie函数if ( ! function_exists('GetCookie')) { function GetCookie($key) { global $cfg_cookie_encode; if( !isset($_COOKIE[$key]) || !isset($_COOKIE[$key.'__ckMd5']) ) { return ''; } else { if($_COOKIE[$key.'__ckMd5']!=substr(md5($cfg_cookie_encode.$_COOKIE[$key]),0,16)) { return ''; } else { return $_COOKIE[$key]; } } } } 它不但读了cookie还验证了md5值。这样,由于index.php中我们可以控制返回一个输入值和这个输入值经过服务器处理后的md5值。那么如果我们伪造DedUserID和它对应的MD5就行了。最后一个问题,因为我们上面是通过用户名伪造ID的,用户名为字符串而ID为整数,但好在在构造用户类中将M_ID intval了一下$this->M_ID = intval($this->M_ID); 那么这么说,如果我们想伪造ID为1的用户的Md5,我们只要在上面设置uid(用户名)为'000001'即可。复现现在我们的思路就是先从member/index.php中获取伪造的DedeUserID和它对于的md5使用它登录访问member/index.php?uid=0000001并抓包(注意cookie中last_vid值应该为空)。可以看到已经获取到了,拿去当做DeDeUserID可以看到,登陆了admin用户。修复意见M_ID被intval后还要判断是否与未intval之前相同。4. Dedecms V5.7后台的两处getshell漏洞信息CVE-2018-9175,提交时间:20180401漏洞成因后台写配置文件过滤不足导致写shell。漏洞代码分析第一个在/dede/sys_verifies.php中的第152行处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中。dedecms对于输入是全局过滤的,在/common.inc.php中注册并过滤了外部提交的变量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); foreach(Array('_GET','_POST','_COOKIE') as $_request) { foreach($$_request as $_k => $_v) { if($_k == 'nvarname') ${$_k} = $_v; else ${$_k} = _RunMagicQuotes($_v); } } } 上面的$refiles就是注册的外部变量,可见已经addlashes了而我们还是需要绕过fwrite($fp, '$files['.$i.'] = "'.$filename.'";'."\r\n"); 实现注入shell,首先需要注入就必须闭合双引号,在这里有个诡异的操作$filename = substr($filename,3,strlen($filename)-3);去掉了输入的前三个字符,这样就为我们写shell制造了机会,当我们输入\" 时经过addlashes会变成\\\",再去掉前三个字符就只剩下双引号实现闭合。此时写入shell后只要再找一个包含modifytmp.inc文件的文件就好了,全局搜索一下可以发现就在本文件/dede/sys_verifies.php第二个同样是写配置文件,位于/dede/sys_cache_up.phpelse if($step == 2) { include_once(DEDEINC."/enums.func.php"); WriteEnumsCache(); //WriteAreaCache(); 已过期 ShowMsg("成功更新枚举缓存,准备更新调用缓存...", "sys_cache_up.php?dopost=ok&step=3&uparc=$uparc"); exit(); } 跟进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(转载请注明来自
创建帐户或登录后发表意见