点击右上角【关注】发哥微课堂头条号,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,然后发送给服务器,截图如下:
然后,查看数据库,发现成功替换,如下图:
0x03:修复建议
其主要原因在于注册时接收数组时数组名称为 $_POST,这样就可以随意添加参数,都会被程序处理,所以可以修改其名称,这样就会成为固定参数,再添加 user_id 就不会生效。
如果把 REPLACE INTO 替换为 INSERT INTO 呢,既然有 REPLACE INTO 的存在,那么肯定有需要的地方,当一个数据表有主键索引时,如果插入一条数据,当主键已经存在时,就会发生冲突报错,有些业务会需要先删除其数据然后再进行插入操作,这个时候 REPLACE INTO 就会派上用场。
閱讀更多 發哥微課堂 的文章