PHP 代碼審計之添加管理員

PHP 代碼審計之添加管理員

點擊右上角【關注】發哥微課堂頭條號,get更多相關技能~


0x00:前言

cms 註冊用戶時,存在添加管理員漏洞,記錄如下。

0x01:代碼追蹤

首先,前臺註冊普通賬號時,url 地址為 / user/regin.php,打開 regin.php 發現,剛開始是接收註冊數據,並進行一些校驗,代碼如下:

if(!check::CheckUser($_POST['user_name'])) {
check::AlertExit("輸入的用戶名必須是4-21字符之間的數字、字母!",-1);
}
$strWhere = "where email='".$_POST['email']."'";
$arrInfo = check::getAPI('mcenter','getUserWhere',"$strWhere^user_id");
if(!empty($arrInfo)){
check::AlertExit("錯誤:該郵箱已被使用!",-1);
}
$strWhere = "where user_name='".$_POST['user_name']."'";
$arrInfo = check::getAPI('mcenter','getUserWhere',"$strWhere^user_id");
if(!empty($arrInfo)){
check::AlertExit("錯誤:用戶名已被註冊!",-1);
}
$arrIllegal=array('admin','管理員','客服');
foreach($arrIllegal as $v){
if(stripos($_POST['user_name'],$v)!==false) {
check::AlertExit("輸入的登錄帳號包含非法字符!",-1);
}

}
if(!is_numeric($_POST['mobile']) or strlen($_POST['mobile'])>12) {
check::AlertExit("電話必須為數字並且不能大於12!",-1);
}
if(!check::CheckPost($_POST['postcode'])) {
check::AlertExit("郵編不符合要求",-1);
}
if(!check::CheckPassword($_POST['password'])) {
check::AlertExit("輸入的密碼必須是4-21字符之間的數字、字母!",-1);
}
if($_POST['password']!=$_POST['password_c']) {
check::AlertExit("兩次輸入的密碼不一致!",-1);
}
if($_POST['authCode'] != $_SESSION['captcha']){
check::AlertExit("錯誤:驗證碼不匹配!",-1);
}

都是一些基本的校驗,且用戶名過濾了特殊符號,繼續往下,處理代碼如下:

$_POST['user_name'] = strip_tags(trim($_POST['user_name']));
if(!empty($arrGWeb['user_pass_type'])) $_POST['password'] = check::strEncryption($_POST['password'],$arrGWeb['jamstr']);
else $_POST['password'] = $_POST['password'];
$_POST['real_name'] = strip_tags(trim($_POST['real_name']));
$_POST['nick_name'] = strip_tags(trim($_POST['nick_name']));
$_POST['postcode'] = $_POST['postcode'];
$_POST['mobile'] = $_POST['mobile'];
$_POST['email'] = $_POST['email'];
$_POST['corp_name'] = $_POST['corp_name'];
$_POST['contact_address'] = $_POST['contact_address'];
$_POST['question'] = $_POST['question'];
$_POST['answer'] = $_POST['answer'];
$_POST['sex'] = $_POST['sex'];
$_POST['tel'] = $_POST['tel'];
$_POST['province'] = $_POST['province'];
$_POST['city'] = $_POST['city'];
$_POST['area'] = $_POST['area'];
$_POST['user_ip'] = check::getIP();
$_POST['submit_date'] = date('Y-m-d H:i:s');
$_POST['session_id'] = session_id();
$intID = $objWebInit->saveInfo($_POST,0,false,true);

if ($intID) {
$_SESSION['user_id'] = $intID;
$_SESSION = array_merge($_SESSION,$_POST);
$arrTemp['user_id'] = $intID;
$arrTemp['add_date'] = date('Y-m-d H:i:s');
$strData = check::getAPIArray($arrTemp);
check::getAPI('mcenter','updateUser',$strData);
echo "";
exit ();
} else {
check::AlertExit('註冊失敗',-1);
}

}

功能是將其數據放到了 $_POST 數組中,然後通過 saveInfo 函數進行了保存,那麼跟蹤其函數,代碼如下:

function saveInfo($arrData,$isModify=false,$isAlert=true,$isMcenter=false){ 
if($isMcenter){
$strData = check::getAPIArray($arrData);
if(!$intUserID = check::getAPI('mcenter','saveInfo',"$strData^$isModify^false")){
if($isAlert) check::AlertExit("與用戶中心通訊失敗,請稍後再試!",-1);
return 0;
}
}
$arr = array();
$arr = check::SqlInjection($this->saveTableFieldG($arrData,$isModify));
if($isModify == 0){
if(!empty($intUserID)) $arr['user_id'] = $intUserID;
if($this->insertUser($arr)){
if(!empty($intUserID)) return $intUserID;
else return $this->lastInsertIdG();
}else{
if($blAlert) check::Alert("新增失敗");
return false;
}
}else{
if($this->updateUser($arr) !== false){
if($isAlert) check::Alert("修改成功!");
else return true;
}else{
if($blAlert) check::Alert("修改失敗");

return false;
}
}
}

這個函數的第一個參數 $arrData 是一個數組,也就是 regin.php 傳過來的註冊用戶信息的 $_POST 數組,最後一個參數 isMcenter 在註冊頁面被調用時傳入的 true,所以程序會走到 check::getAPI 這裡,這個寫法第一個參數傳入的模塊名稱,第二個參數傳入的是方法名,找到這個模塊中的這個方法,其代碼如下:

function saveInfo($arrData,$isModify=false,$isAlert=true){
$arr = array();
$arr = check::SqlInjection($this->saveTableFieldG($arrData,$isModify));

if($isModify == 0){
return $this->insertUser($arr);
}else{
if($this->updateUser($arr) !== false){
if($isAlert) check::Alert("修改成功!");
return true;
}else{
if($blAlert) check::Alert("修改失敗!");
return false;
}
}
}

在這個函數中,首先通過 check::SqlInjection 過濾了可能造成 sql 注入的危險字符,過濾通過後,通過 insertUser 函數進行了插入用戶的操作,跟蹤此函數,代碼如下:

public function insertUser($arrData){
$strSQL = "REPLACE INTO $this->tablename1 (";
$strSQL .= '`';
$strSQL .= implode('`,`', array_keys($arrData));

$strSQL .= '`)';
$strSQL .= " VALUES ('";
$strSQL .= implode("','",$arrData);
$strSQL .= "')";
if ($this->db->exec($strSQL)) {
return $this->db->lastInsertId();
} else {
return false ;
}
}

這裡只有一個參數 $arrData,這個參數傳來的數據,就是註冊頁面一開始接收的用戶數據 $_POST 數組,然後將相關數據進行了 sql 語句的拼接。至此,這個註冊流程就走完了。

在拼接 sql 語句時,程序使用了 REPLACE INTO,而不是 INSERT INTO,根據字面理解一個是替換數據,一個是插入數據。他們的區別在於 replace into 首先會嘗試插入數據到表中,如果發現表中已經有此數據(通過主鍵或者唯一索引去判斷),則會先進行刪除此數據,然後插入新數據,否則的話會直接進行插入操作。

這樣的話,就會存在一個註冊管理賬號的漏洞,在程序中 user_id 為 1 的默認是管理員。而註冊用戶時,接收的數據是通過 $_POST 接收的,而並非固定的參數,這樣在註冊時攔截數據報可以增加一個 user_id 的參數,其值為 1,然後再通過 REPLACE INTO 語句進行插入,則可以成功替換掉管理員的賬號。

0x02:滲透驗證

首先,前臺註冊賬號,然後攔截數據報,隨後在參數中添加 user_id,其值為 1,然後發送給服務器,截圖如下:

PHP 代碼審計之添加管理員

然後,查看數據庫,發現成功替換,如下圖:

PHP 代碼審計之添加管理員

0x03:修復建議

其主要原因在於註冊時接收數組時數組名稱為 $_POST,這樣就可以隨意添加參數,都會被程序處理,所以可以修改其名稱,這樣就會成為固定參數,再添加 user_id 就不會生效。

如果把 REPLACE INTO 替換為 INSERT INTO 呢,既然有 REPLACE INTO 的存在,那麼肯定有需要的地方,當一個數據表有主鍵索引時,如果插入一條數據,當主鍵已經存在時,就會發生衝突報錯,有些業務會需要先刪除其數據然後再進行插入操作,這個時候 REPLACE INTO 就會派上用場。


分享到:


相關文章: