PHP 代码审计之添加管理员

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 就会派上用场。


                                                                        公众号推荐:aFa攻防实验室

                         分享关于信息搜集、Web安全、内网安全、代码审计、红蓝对抗、Java、Python等方面的东西。

                                                                              

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值