?

May 19 2018

ThinkPHP 框架SQL注入技術分析

首頁 » 代碼審計 » ThinkPHP 框架SQL注入技術分析   

t01c5f7fda3eaf1fa33.jpg


4月12號,ThinkPHP官方團隊發布“ThinkPHP5.0.17&5.1.9版本發布——包含安全更新”通知,提醒用戶第一時間更新框架版本,在這次更新中,包含了對由360企業安全集團代碼衛士團隊報送的一個高危安全漏洞的修復。本文針對該漏洞的技術細節進行分析。

 

簡要描述

ThinkPHP是一個免費開源的,快速、簡單的面向對象的輕量級PHP開發框架,是為了敏捷WEB應用開發和簡化企業應用開發而誕生的。ThinkPHP從誕生的12年間一直秉承簡潔實用的設計原則,在保持出色的性能和至簡的代碼的同時,也注重易用性。目前ThinkPHP框架是國內使用量最大的框架之一,國內用戶量眾多。

近日,360企業安全集團代碼衛士團隊安全研究人員發現該框架V5.1.7-V5.1.8 版本在底層數據處理驅動解析數據的時候存在缺陷,一定場景下,攻擊者可以通過構造惡意數據包利用SQL注入的方式獲取用戶數據庫內容。360企業安全集團代碼衛士團隊已第一時間和ThinkPHP團隊進行溝通修復,建議相關用戶及時更新官方發布的新版本。

 

漏洞分析

注:該漏洞ThinkPHP官方團隊在報送當天(2018-04-06)緊急進行了修復處理,詳細請參考:https://github.com/top-think/framework/commit/39bb0fe6d50ee77e0779f646b10bce08c442a5e3

以下漏洞分析基于ThinkPHP V5.1.8(2018-04-05未更新版)

這里我們主要跟進分析執行update操作的過程。為了方便理解,先直接放出函數的調用棧。

Mysql.php:200, thinkdbbuilderMysql->parseArrayData()

Builder.php:147, thinkdbBuilder->parseData()

Builder.php:1139, thinkdbBuilder->update()

Connection.php:1149, thinkdbConnection->update()

Query.php:2571, thinkdbQuery->update()

Index.php:18, appindexcontrollerIndex->testsql()

Container.php:285, ReflectionMethod->invokeArgs()

Container.php:285, thinkContainer->invokeReflectMethod()

Module.php:139, thinkroutedispatchModule->run()

Url.php:31, thinkroutedispatchUrl->run()

App.php:378, thinkApp->think{closure}()

Middleware.php:119, call_user_func_array:{C:wamp64wwwthink518thinkphplibrarythinkMiddleware.php:119}()

Middleware.php:119, thinkMiddleware->think{closure}()

Middleware.php:74, call_user_func:{C:wamp64wwwthink518thinkphplibrarythinkMiddleware.php:74}()

Middleware.php:74, thinkMiddleware->dispatch()

App.php:399, thinkApp->run()

index.php:21, {main}()

缺陷關鍵點為thinkphp解析用戶傳遞過來的Data可控,且可以繞過安全檢查。

根據文件 Connection.php:1149,thinkdbConnection->update()第1102行update函數分析,這個函數的主要功能是用于執行update SQL語句。

//Connection.php:1149, thinkdbConnection->update()

public function update(Query $query)

{

$options = $query->getOptions(); if (isset($options['cache']) && is_string($options['cache']['key'])) {

       $key = $options['cache']['key'];

   }


   $pk   = $query->getPk($options);

   $data = $options['data']; if (empty($options['where'])) { // 如果存在主鍵數據 則自動作為更新條件 if (is_string($pk) && isset($data[$pk])) {

           $where[$pk] = [$pk, '=', $data[$pk]]; if (!isset($key)) {

               $key = $this->getCacheKey($query, $data[$pk]);

           } unset($data[$pk]);

       } elseif (is_array($pk)) { // 增加復合主鍵支持 foreach ($pk as $field) { if (isset($data[$field])) {

                   $where[$field] = [$field, '=', $data[$field]];

               } else { // 如果缺少復合主鍵數據則不執行 throw new Exception('miss complex primary data');

               } unset($data[$field]);

           }

       } if (!isset($where)) { // 如果沒有任何更新條件則不執行 throw new Exception('miss update condition');

       } else {

           $options['where']['AND'] = $where;

           $query->setOption('where', ['AND' => $where]);

       }

   } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) {

       $key = $this->getCacheKey($query, $options['where']['AND'][$pk]);

   } // 更新數據 $query->setOption('data', $data); // 生成UPDATE SQL語句 $sql  = $this->builder->update($query);

   $bind = $query->getBind(); if (!empty($options['fetch_sql'])) { // 獲取實際執行的SQL語句 return $this->getRealSql($sql, $bind);

   } // 檢測緩存 $cache = Container::get('cache'); if (isset($key) && $cache->get($key)) { // 刪除緩存 $cache->rm($key);

   } elseif (!empty($options['cache']['tag'])) {

       $cache->clear($options['cache']['tag']);

   } // 執行操作 $result = '' == $sql ? 0 : $this->execute($sql, $bind); if ($result) { if (is_string($pk) && isset($where[$pk])) {

           $data[$pk] = $where[$pk];

       } elseif (is_string($pk) && isset($key) && strpos($key, '|')) { list($a, $val) = explode('|', $key);

           $data[$pk]     = $val;

       }


       $query->setOption('data', $data);

       $query->trigger('after_update');

   } return $result;

}

第1146行, $query->setOption(‘data’,$data);這里將用戶傳遞的 $dataset到 $query變量中,為下一步的生成 UPDATE SQL語句做準備,執行 $sql=$this->builder->update($query);語句,重點馬上要來了,跟進 Builder.php:1139,thinkdbBuilder->update()函數

//Builder.php:1139, thinkdbBuilder->update()

public function update(Query $query)

{

$options = $query->getOptions();


   $table = $this->parseTable($query, $options['table']);

   $data  = $this->parseData($query, $options['data']); if (empty($data)) { return '';

   } foreach ($data as $key => $val) {

       $set[] = $key . ' = ' . $val;

   } return str_replace(

       ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],

       [ $this->parseTable($query, $options['table']),

           implode(' , ', $set), $this->parseJoin($query, $options['join']), $this->parseWhere($query, $options['where']), $this->parseOrder($query, $options['order']), $this->parseLimit($query, $options['limit']), $this->parseLock($query, $options['lock']), $this->parseComment($query, $options['comment']),

       ], $this->updateSql);

}

剛剛我們將用戶可控的 $dataset到 $query[‘options’]中,這里我們先獲取 $query[‘options’]內容到 $options中,然后對Data進行解析 $data=$this->parseData($query,$options[‘data’]);

//Builder.php:147, thinkdbBuilder->parseData()

protected function parseData(Query $query, $data = [], $fields = [], $bind = [], $suffix = ‘’)

{

if (empty($data)) { return [];

   }


   $options = $query->getOptions(); // 獲取綁定信息 if (empty($bind)) {

       $bind = $this->connection->getFieldsBind($options['table']);

   } if (empty($fields)) { if ('*' == $options['field']) {

           $fields = array_keys($bind);

       } else {

           $fields = $options['field'];

       }

   }


   $result = []; foreach ($data as $key => $val) {

       $item = $this->parseKey($query, $key); if ($val instanceof Expression) {

           $result[$item] = $val->getValue(); continue;

       } elseif (!is_scalar($val) && (in_array($key, (array) $query->getOptions('json')) || 'json' == $this->connection->getFieldsType($options['table'], $key))) {

           $val = json_encode($val);

       } elseif (is_object($val) && method_exists($val, '__toString')) { // 對象數據寫入 $val = $val->__toString();

       } if (false !== strpos($key, '->')) { list($key, $name) = explode('->', $key);

           $item             = $this->parseKey($query, $key);

           $result[$item]    = 'json_set(' . $item . ', '$.' . $name . '', ' . $this->parseDataBind($query, $key, $val, $bind, $suffix) . ')';

       } elseif (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 'INC':

                   $result[$item] = $item . ' + ' . floatval($val[1]); break; case 'DEC':

                   $result[$item] = $item . ' - ' . floatval($val[1]); break; default:

                   $value = $this->parseArrayData($query, $val); if ($value) {

                       $result[$item] = $value;

                   }

           }

       } elseif (is_scalar($val)) { // 過濾非標量數據 $result[$item] = $this->parseDataBind($query, $key, $val, $bind, $suffix);

       }

   } return $result;

}

在第115行,通過 foreach($dataas$key=>$val)處理 $data,然后解析 $key保存到 $item變量中去,之后執行下面的判斷邏輯,想要合理地進入各個判斷分支,就要巧妙的構造 $key和 $value也就是 $data的值。緊接著我們進入漏洞觸發點 $value=$this->parseArrayData($query,$val);,跟進函數 $value=$this->parseArrayData($query,$val);

//Mysql.php:200, thinkdbbuilderMysql->parseArrayData()

protected function parseArrayData(Query $query, $data)

{

list($type, $value) = $data; switch (strtolower($type)) { case 'point':

           $fun   = isset($data[2]) ? $data[2] : 'GeomFromText';

           $point = isset($data[3]) ? $data[3] : 'POINT'; if (is_array($value)) {

               $value = implode(' ', $value);

           }

           $result = $fun . '('' . $point . '(' . $value . ')')';//需要簡單的構造一下sql語句 break; default:

           $result = false;

   } return $result;

}

這里 $type、 $value和 $data均為可控值,那么函數返回的 $result也就是可控的。回到上一個 Builder.php文件中,將返回的結果賦值到 $result[$item]=$value;中,之后的生成SQL語句和常見的流程沒有任何差別不再展開具體分析。

 

驗證截圖

t01c26764c136390f7c.png

修復建議

更新受影響ThinkPHP版本到最新版本

 

關于我們

360代碼安全實驗室是360企業安全集團旗下專門從事源代碼、二進制漏洞挖掘和分析的研究團隊,主要研究方向包括:Windows/Linux/MacOS操作系統、應用軟件、開源軟件、網絡設備、IoT設備等。團隊成員既有二進制漏洞挖掘高手,微軟全球TOP100貢獻白帽子,Pwn2own2017冠軍隊員,又有開源軟件安全大拿,人工智能安全專家。實驗室安全團隊的研究成果獲得微軟、Adobe、各種開源組織等的50多次致-謝。

 

參考

ThinkPHP5.0.17&5.1.9版本發布——包含安全更新

(http://www.thinkphp.cn/topic/55757.html)

如果您喜歡本博客,歡迎點擊圖片定訂閱到郵箱填寫您的郵件地址,訂閱我們的精彩內容:

正文部分到此結束

文章標簽: thinkphp漏洞

版權聲明:若無特殊注明,本文皆為( mOon )原創,轉載請保留文章出處。

也許喜歡: «ThinkPHP3.x/5.x框架任意文件包含 | Thinkphp3.2.3最新版update注入漏洞»

你腫么看?

你還可以輸入 250/250 個字

? 微笑 大笑 拽 大哭 親親 流汗 噴血 奸笑 囧 不爽 暈 示愛 害羞 吃驚 驚嘆 愛你 嚇死了 呵呵

評論信息框

這篇文章還沒有收到評論,趕緊來搶沙發吧~

?
?
河北11选5开奖