点击右上角【关注】发哥微课堂头条号,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 \”<script>alert(\’注册完成\’);window.location=\'{$arrGWeb[\’WEB_ROOT_pre\’]}/\’;</script>\”; 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,然后发送给服务器,截图如下:
然后,查看数据库,发现成功替换,如下图:
0x03:修复建议
其主要原因在于注册时接收数组时数组名称为 $_POST,这样就可以随意添加参数,都会被程序处理,所以可以修改其名称,这样就会成为固定参数,再添加 user_id 就不会生效。
如果把 REPLACE INTO 替换为 INSERT INTO 呢,既然有 REPLACE INTO 的存在,那么肯定有需要的地方,当一个数据表有主键索引时,如果插入一条数据,当主键已经存在时,就会发生冲突报错,有些业务会需要先删除其数据然后再进行插入操作,这个时候 REPLACE INTO 就会派上用场。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。