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 == 1dedecms的/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?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.php
else 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(empty($issign)) WriteEnumsJs($egroup);}return '成功更新所有枚舉緩存!'; }可以看到,直接從數據庫中讀取并寫入php文件中,從數據庫中取出后并沒有經過過濾。
將shell寫進數據庫中https://172.16.180.130/dedecms/uploads/dede/stepselect_main.php?action=addenum_save&ename=123&egroup=;phpinfo();//&islogin=1
復現
因為包含是在同一個文件,所以直接輸入dede/sys_verifies.php?action=getfiles&refiles[]=123&refiles[]=\%22;phpinfo();die();//?就可以。
修復意見
更改前面的過濾條件
5. 某cms v5.7 sp2 后臺文件上傳 getshell
漏洞信息
CVE-2019-8362,提交時間 20190216
漏洞成因
上傳zip文件解壓縮對于文件名過濾不周,導致getshell
漏洞代碼分析
/dede/album_add.php 175行驗證后綴
$fm->GetMatchFiles($tmpzipdir,"jpg|png|gif",$imgs);
進入函數:
function GetMatchFiles($indir, $fileexp, &$filearr){$dh = dir($indir);while($filename = $dh->read()){$truefile = $indir.'/'.$filename;if($filename == "." || $filename == ".."){continue;}else if(is_dir($truefile)){$this->GetMatchFiles($truefile, $fileexp, $filearr);}else if(preg_match("/\.(".$fileexp.")/i",$filename)){$filearr[] = $truefile;}}$dh->close();}可以確定preg_match("/\.(".$fileexp.")/i",$filename)只是判斷了文件名中是否存在.jpg、.png、.gif中的一個,只要構造1.jpg.php就可以繞過。
復現
管理員用戶上傳壓縮了1.jpg.php這個shell的zip文件再解壓縮即可。這里
修復意見
加強過濾正則的書寫。
同和上面,后臺dedecms后臺getshell真的很雞肋。
總結
以上是生活随笔為你收集整理的Dedecms 最新版漏洞收集并复现学习的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux下,MySQL默认的数据文档存
- 下一篇: JMS 的Pub/Sub模型