本文作者:myndtt
1 、版本 4.0.181010
2、下載鏈接:
http://daicuo.co/forum-1653-1-1.html
3、前臺可註冊用戶
漏洞詳情
註冊處
用戶註冊一個賬號對應處理函數爲:
Lib\Lib\Action\Home\UserAction.class.php
文件下的 post 函數。
public function post(){ #var_dump($_POST); 測試 $info = D("User")->ff_update($_POST);#跟進 #var_dump($info);測試 if($info){ //註冊積分 if(C('user_register_score')){ D('Score')->ff_user_score($info['user_id'], 2, intval(C('user_register_score'))); } //推廣積分 if($info['user_pid'] && C('user_register_score_pid')){ #echo '1';#測試 D('Score')->ff_user_score($info['user_pid'], 4, intval(C('user_register_score_pid'))); } //json返回 $data = array('id'=>$info['user_id'],'referer'=>cookie('ff_register_referer')); //歡迎郵件信息 if( C('user_register_welcome') ){ $content = str_replace(array('{username}','{sitename}','{time}'), array($info['user_name'],C('site_name'),time()), C('user_register_welcome')); D("Email")->send($info['user_email'], $info['user_name'], $info['user_name'].'您好,感謝您的註冊', $content); } //返回註冊結果 if (C('user_register_check')) { $this->ajaxReturn($data, "我們會盡快審覈你的註冊!", 201); }else{ $this->ajaxReturn($data, "感謝你的註冊!", 200); } }else{ $this->ajaxReturn(0, D("User")->getError(), 500); } }
該函數直接將 post 的數據傳入,則跟進ff_update
函數至\Lib\Lib\Model\UserModel.class.php
文件
public function ff_update($data, $group='home'){ // 創建安全數據對象TP $data = $this->create($data);#對字段進行驗證 if(false === $data){ $this->error = $this->getError(); return false; } /* 添加或修改行爲 */ if(empty($data['user_id'])){ $data['user_id'] = $this->add(); if(!$data['user_id']){ $this->error = $this->getError(); return false; } if($group == 'home'){ //寫入註冊時間防刷新註冊 cookie('ff_register_time', time()); //寫入登錄信息 $this->ff_login_write(array('user_id'=>$data['user_id'],'user_name'=>$data['user_name'],'user_pwd'=>$data['user_pwd'])); } } else { $status = $this->save(); if(false === $status){ $this->error = $this->getError(); return false; } } return $data; }
跟進create
函數,來到\Lib\Think\Core\Model.class.php
文件
public function create($data='',$type='') { // 如果沒有傳值默認取POST數據 if(empty($data)) { $data = $_POST; }elseif(is_object($data)){ $data = get_object_vars($data); }elseif(!is_array($data)){ $this->error = L('_DATA_TYPE_INVALID_'); return false; } // 狀態 $type = $type?$type:(!empty($data[$this->getPk()])?self::MODEL_UPDATE:self::MODEL_INSERT); // 表單令牌驗證 if(C('TOKEN_ON') && !$this->autoCheckToken($data)) { $this->error = L('_TOKEN_ERROR_'); return false; } // 檢查字段映射 if(!empty($this->_map)) { foreach ($this->_map as $key=>$val){ if(isset($data[$key])) { $data[$val] = $data[$key]; unset($data[$key]); } } } // 數據自動驗證 if(!$this->autoValidation($data,$type)) return false;#對傳入數據進行驗證 // 驗證完成生成數據對象 $vo = array(); foreach ($this->fields as $key=>$name){ if(substr($key,0,1)=='_') continue; $val = isset($data[$name])?$data[$name]:null; //保證賦值有效 if(!is_null($val)){ $vo[$name] = (MAGIC_QUOTES_GPC && is_string($val))? stripslashes($val) : $val; } } // 創建完成對數據進行自動處理 $this->autoOperation($vo,$type); // 賦值當前數據對象 $this->data = $vo; // 返回創建的數據以供其他調用 return $vo; }
跟進 autoValidation
函數查看程序如何對數據進行驗證
protected function autoValidation($data,$type) { // 屬性驗證 if(!empty($this->_validate)) { // 如果設置了數據自動驗證 // 則進行數據驗證 // 重置驗證錯誤信息 foreach($this->_validate as $key=>$val) {#程序需要驗證的事務 // 驗證因子定義格式 // array(field,rule,message,condition,type,when,params) // 判斷是否需要執行驗證 if(empty($val[5]) || $val[5]== self::MODEL_BOTH || $val[5]== $type ) { if(0==strpos($val[2],'{%') && strpos($val[2],'}')) // 支持提示信息的多語言 使用 {%語言定義} 方式 $val[2] = L(substr($val[2],2,-1)); $val[3] = isset($val[3])?$val[3]:self::EXISTS_VAILIDATE; $val[4] = isset($val[4])?$val[4]:'regex'; // 判斷驗證條件 switch($val[3]) { case self::MUST_VALIDATE: // 必須驗證 不管表單是否有設置該字段 if(false === $this->_validationField($data,$val)){ $this->error = $val[2]; return false; } break; case self::VALUE_VAILIDATE: // 值不爲空的時候才驗證 if('' != trim($data[$val[0]])){ if(false === $this->_validationField($data,$val)){ $this->error = $val[2]; return false; } } break; default: // 默認表單存在該字段就驗證 if(isset($data[$val[0]])){#字段爲空就可以繞過檢測 if(false === $this->_validationField($data,$val)){ $this->error = $val[2]; return false; } } } } } } return true; }
需要驗證的事務有
protected $_validate = array( // 防刷新註冊 array('user_register','validate_user_register','註冊速度過快!',1,'callback',1), // 驗證呢稱 array('user_name','require','用戶呢稱必須填寫!',0,'',3), array('user_name', '', '用戶呢稱被佔用,請重新填寫', 2, 'unique',3),#後面要進行驗證 /* 驗證郵箱 */ array('user_email', 'email', ' ', 0,'',3), array('user_email', '', '郵箱被佔用,請重新填寫', 0, 'unique',3),#後面要進行驗證 /* 驗證密碼 */ array('user_pwd_re', 'user_pwd', '兩次密碼輸入不一樣', 2, 'confirm'), //兩次密碼輸入不一樣! );
則需要驗證的字段有 user_name,user_name,user_email,user_pwd_re,user_pwd
. 這些都是我們正常註冊需要填寫的數據,當然也是我們可以控制的數據,因爲它們都取自於$_POST
。這時候我們來看default
的部分:if(isset($data[$val[0]]))
只要傳入的數據爲空就不必進入檢測了,這樣會帶來問題。
接着繼續來看看_validationField
函數吧
protected function _validationField($data,$val) { switch($val[4]) { case 'function':// 使用函數進行驗證 case 'callback':// 調用方法進行驗證 $args = isset($val[6])?$val[6]:array(); array_unshift($args,$data[$val[0]]); if('function'==$val[4]) { return call_user_func_array($val[1], $args); }else{ return call_user_func_array(array(&$this, $val[1]), $args); } case 'confirm': // 驗證兩個字段是否相同 return $data[$val[0]] == $data[$val[1]]; case 'in': // 驗證是否在某個數組範圍之內 return in_array($data[$val[0]] ,$val[1]); case 'equal': // 驗證是否等於某個值 return $data[$val[0]] == $val[1]; case 'unique': // 驗證某個值是否唯一 if(is_string($val[0]) && strpos($val[0],',')) $val[0] = explode(',',$val[0]); $map = array(); if(is_array($val[0])) { // 支持多個字段驗證 foreach ($val[0] as $field) $map[$field] = $data[$field]; }else{ $map[$val[0]] = $data[$val[0]]; } if(!empty($data[$this->getPk()])) { // 完善編輯的時候驗證唯一 $map[$this->getPk()] = array('neq',$data[$this->getPk()]);#真正問題所在! } if($this->where($map)->find()) return false; break; case 'regex': default: // 默認使用正則驗證 可以使用驗證類中定義的驗證名稱 // 檢查附加規則 return $this->regex($data[$val[0]],$val[1]); } return true; }
不太清楚爲什麼程序在驗證字段是否唯一的時候爲什麼要加入這段
if(!empty($data[$this->getPk()])) { // 完善編輯的時候驗證唯一 $map[$this->getPk()] = array('neq',$data[$this->getPk()]);#問題所在! } if($this->where($map)->find()) return false;
$this->getPk()
函數是得到當前要判斷的字段所在表的主鍵名稱(註冊時影響的表即爲 ff_user,主鍵爲 user_id。在thinkphp 中也有該函數)。如果存在,那麼就用 'neq', 也即不等於。這裏需要出現黑人問號?。等於說註冊的時候我傳入一個字段user_id
就可以做一些事情了。例如下圖
如果已經註冊了一個user_name=myndtt
並且user_id=2
的用戶,那麼這樣就完全繞過了字段驗證。或者只需要傳入user_id
這個字段就可以繞過了。字段驗證完以後沒問題就會更新數據庫了。例如下圖(這裏沒有傳入 user_name, user_email 等字段,僅僅傳入了 user_id 和密碼),那麼程序就會對user_id
對應的用戶進行密碼更改。
同時網站可以通過user_id
來遍歷得到註冊用戶的user_name
。可以檢測 user_id 是否存在。如
總之就可以利用user_id
來更改ff_user
表中的許多
字段。
接着回到最早的post
函數
if($info){#得到註冊積分 //註冊積分 if(C('user_register_score')){ D('Score')->ff_user_score($info['user_id'], 2, intval(C('user_register_score'))); } //推廣積分 if($info['user_pid'] && C('user_register_score_pid')){ #echo '';#測試 D('Score')->ff_user_score($info['user_pid'], 4, intval(C('user_register_score_pid'))); } //json返回 $data = array('id'=>$info['user_id'],'referer'=>cookie('ff_register_referer')); //歡迎郵件信息 if( C('user_register_welcome') ){ $content = str_replace(array('{username}','{sitename}','{time}'), array($info['user_name'],C('site_name'),time()), C('user_register_welcome')); D("Email")->send($info['user_email'], $info['user_name'], $info['user_name'].'您好,感謝您的註冊', $content); }
如果user_id=自己的id
話就可以無限註冊給自己加分了。
那麼問題來了,爲什麼不直接:加上一個 user_score 字段呢。如 post user_id=2&user_score=30000
。
回到post
函數
$info = D("User")->ff_update($_POST);#執行完後表中user_id對應user_score爲30000 #var_dump($info);測試 if($info){ //註冊積分 if(C('user_register_score')){ D('Score')->ff_user_score($info['user_id'], 2, intval(C('user_register_score')));#此時用給會計算ff_score表中對應id的score。以此爲基礎加上註冊#的分數 }
遺憾的是D('Score')->ff_user_score($info['user_id'], 2, intval(C('user_register_score')));
會繼續更新一次。在此中可以考慮時間競爭獲得高額積分,否則就一次次發包,每次獲得註冊獎勵的分數。
登入處
上述的更改用戶密碼,看似不能直接可以登入前臺(登入需要郵箱),因爲只能獲得user_name
。
來到處理登入處的邏輯代碼部分
public function loginpost(){ $user_id = D("User")->ff_login($_POST);#不好的現象 if($user_id){ $this->ajaxReturn($user_id, "登錄成功", 200); }else{ $this->ajaxReturn(0, D("User")->getError(), 500); } }
進入ff_login
函數
public function ff_login($post){ $where = array(); //用戶名與郵箱登錄 if(filter_var($post['user_email'], FILTER_VALIDATE_EMAIL)){#如果user_email不符合#email的正則 $where['user_email'] = array('eq', htmlspecialchars(trim($post['user_email']))); }else{ $where['user_name'] = array('eq', #那麼考慮用戶輸入的user_email可能是user_name htmlspecialchars(trim($post['user_email']))); } //查庫 $info = $this->field('user_id,user_name,user_pwd,user_email,user_status')->where($where)->find(); if(!$info){
這種選擇,考慮如果用戶輸入的不是郵箱就是用戶名,經常在該一些 cms 中出現。可能在一種程度上方便了用戶,但是也帶來隱患。這裏就是可以用 user_name 直接登入
危害總結
1、任意前臺用戶密碼重置
2、任意用戶刷分(影幣)
3、用戶其他數據的更改(頭像鏈接,之類等)
修改
1、註冊,登入處沒必要用$_POST
直接獲取所有的 post 數據,多寫幾條代碼,拿到自己想要的就好。
2、驗證字段爲空處的處理邏輯有問題,不空才檢測,應當做限制。
3、驗證具體字段唯一的時候何必去請求主鍵。
小結
像這種前臺用戶修改數據的地方往往是比較容易出現越權的地方。程序員爲了方便,一次性獲取所有用戶 POST 的數據,沒考慮用戶在修改某一些字段的同時沒其他字段數據是不是也會被修改,也很少考慮修改的數據是不是當前登入的用戶。黑盒測試時,容易發現,白盒測試時,需要一段時間調試找到具體關鍵問題點。