FeiFeiCms 前台逻辑漏洞分析

本文作者:myndtt

1 、版本 4.0.181010

2、下载链接:

http://daicuo.co/forum-1653-1-1.html

3、前台可注册用户

漏洞详情

注册处

用户注册一个账号对应处理函数为:

Lib\Lib\Action\Home\UserAction.class.php

文件下的 post 函数。

public function post(){
    #var_dump($_POST); 测试
    $info = D("User")->ff_update($_POST);#跟进
    #var_dump($info);测试
    if($info){
        //注册积分
        if(C('user_register_score')){
            D('Score')->ff_user_score($info['user_id'], 2, intval(C('user_register_score')));
        }
        //推广积分
        if($info['user_pid'] && C('user_register_score_pid')){
     #echo '1';#测试
            D('Score')->ff_user_score($info['user_pid'], 4, intval(C('user_register_score_pid')));
        }
        //json返回
        $data = array('id'=>$info['user_id'],'referer'=>cookie('ff_register_referer'));
        //欢迎邮件信息
        if( C('user_register_welcome') ){
            $content = str_replace(array('{username}','{sitename}','{time}'), array($info['user_name'],C('site_name'),time()), C('user_register_welcome'));
            D("Email")->send($info['user_email'], $info['user_name'], $info['user_name'].'您好,感谢您的注册', $content);
        }
        //返回注册结果
        if (C('user_register_check')) {
            $this->ajaxReturn($data, "我们会尽快审核你的注册!", 201);
        }else{
            $this->ajaxReturn($data, "感谢你的注册!", 200);
        }
    }else{
        $this->ajaxReturn(0, D("User")->getError(), 500);
    }
}

该函数直接将 post 的数据传入,则跟进ff_update函数至\Lib\Lib\Model\UserModel.class.php文件

public function ff_update($data, $group='home'){
   // 创建安全数据对象TP
   $data = $this->create($data);#对字段进行验证
   if(false === $data){
      $this->error = $this->getError();
      return false;
   }
   /* 添加或修改行为 */
   if(empty($data['user_id'])){
      $data['user_id'] = $this->add();
      if(!$data['user_id']){
         $this->error = $this->getError();
         return false;
      }
      if($group == 'home'){
         //写入注册时间防刷新注册
         cookie('ff_register_time', time());
         //写入登录信息
         $this->ff_login_write(array('user_id'=>$data['user_id'],'user_name'=>$data['user_name'],'user_pwd'=>$data['user_pwd']));
      }
   } else {
      $status = $this->save();
      if(false === $status){
         $this->error = $this->getError();
         return false;
      }
   }
   return $data;
}

跟进create函数,来到\Lib\Think\Core\Model.class.php文件

public function create($data='',$type='') {
   // 如果没有传值默认取POST数据
   if(empty($data)) {
       $data    =   $_POST;
   }elseif(is_object($data)){
       $data   =   get_object_vars($data);
   }elseif(!is_array($data)){
       $this->error = L('_DATA_TYPE_INVALID_');
       return false;
   }
   // 状态
   $type = $type?$type:(!empty($data[$this->getPk()])?self::MODEL_UPDATE:self::MODEL_INSERT);

   // 表单令牌验证
   if(C('TOKEN_ON') && !$this->autoCheckToken($data)) {
       $this->error = L('_TOKEN_ERROR_');
       return false;
   }

   // 检查字段映射
   if(!empty($this->_map)) {
       foreach ($this->_map as $key=>$val){
           if(isset($data[$key])) {
               $data[$val] =   $data[$key];
               unset($data[$key]);
           }
       }
   }

   // 数据自动验证
   if(!$this->autoValidation($data,$type)) return false;#对传入数据进行验证

   // 验证完成生成数据对象
   $vo   =  array();
   foreach ($this->fields as $key=>$name){
       if(substr($key,0,1)=='_') continue;
       $val = isset($data[$name])?$data[$name]:null;
       //保证赋值有效
       if(!is_null($val)){
           $vo[$name] = (MAGIC_QUOTES_GPC && is_string($val))?   stripslashes($val)  :  $val;
       }
   }
   // 创建完成对数据进行自动处理
   $this->autoOperation($vo,$type);
   // 赋值当前数据对象
   $this->data =   $vo;
   // 返回创建的数据以供其他调用
   return $vo;
}

跟进 autoValidation 函数查看程序如何对数据进行验证

protected function autoValidation($data,$type) {
    // 属性验证
    if(!empty($this->_validate)) {
        // 如果设置了数据自动验证
        // 则进行数据验证
        // 重置验证错误信息
        foreach($this->_validate as $key=>$val) {#程序需要验证的事务
            // 验证因子定义格式
            // array(field,rule,message,condition,type,when,params)
            // 判断是否需要执行验证
            if(empty($val[5]) || $val[5]== self::MODEL_BOTH || $val[5]== $type ) {
                if(0==strpos($val[2],'{%') && strpos($val[2],'}'))
                    // 支持提示信息的多语言 使用 {%语言定义} 方式
                    $val[2]  =  L(substr($val[2],2,-1));
                $val[3]  =  isset($val[3])?$val[3]:self::EXISTS_VAILIDATE;
                $val[4]  =  isset($val[4])?$val[4]:'regex';
                // 判断验证条件
                switch($val[3]) {
                    case self::MUST_VALIDATE:   // 必须验证 不管表单是否有设置该字段
                        if(false === $this->_validationField($data,$val)){
                            $this->error    =   $val[2];
                            return false;
                        }
                        break;
                    case self::VALUE_VAILIDATE:    // 值不为空的时候才验证
                        if('' != trim($data[$val[0]])){
                            if(false === $this->_validationField($data,$val)){
                                $this->error    =   $val[2];
                                return false;
                            }
                        }
                        break;
                    default:    // 默认表单存在该字段就验证
                        if(isset($data[$val[0]])){#字段为空就可以绕过检测
                            if(false === $this->_validationField($data,$val)){
                                $this->error    =   $val[2];
                                return false;
                            }
                        }
                }
            }
        }
    }
    return true;
}

需要验证的事务有

protected $_validate = array(
        // 防刷新注册
        array('user_register','validate_user_register','注册速度过快!',1,'callback',1),
        // 验证呢称
        array('user_name','require','用户呢称必须填写!',0,'',3),
        array('user_name', '', '用户呢称被占用,请重新填写', 2, 'unique',3),#后面要进行验证
        /* 验证邮箱 */
        array('user_email', 'email', ' ', 0,'',3),
        array('user_email', '', '邮箱被占用,请重新填写', 0, 'unique',3),#后面要进行验证
        /* 验证密码 */
        array('user_pwd_re', 'user_pwd', '两次密码输入不一样', 2, 'confirm'), //两次密码输入不一样!
    );

则需要验证的字段有 user_name,user_name,user_email,user_pwd_re,user_pwd. 这些都是我们正常注册需要填写的数据,当然也是我们可以控制的数据,因为它们都取自于$_POST。这时候我们来看default的部分:if(isset($data[$val[0]]))只要传入的数据为空就不必进入检测了,这样会带来问题。

接着继续来看看_validationField函数吧

protected function _validationField($data,$val) {
    switch($val[4]) {
        case 'function':// 使用函数进行验证
        case 'callback':// 调用方法进行验证
            $args = isset($val[6])?$val[6]:array();
            array_unshift($args,$data[$val[0]]);
            if('function'==$val[4]) {
                return call_user_func_array($val[1], $args);
            }else{
                return call_user_func_array(array(&$this, $val[1]), $args);
            }
        case 'confirm': // 验证两个字段是否相同
            return $data[$val[0]] == $data[$val[1]];
        case 'in': // 验证是否在某个数组范围之内
            return in_array($data[$val[0]] ,$val[1]);
        case 'equal': // 验证是否等于某个值
            return $data[$val[0]] == $val[1];
        case 'unique': // 验证某个值是否唯一
            if(is_string($val[0]) && strpos($val[0],','))
                $val[0]  =  explode(',',$val[0]);
            $map = array();
            if(is_array($val[0])) {
                // 支持多个字段验证
                foreach ($val[0] as $field)
                    $map[$field]   =  $data[$field];
            }else{
                $map[$val[0]] = $data[$val[0]];
            }
            if(!empty($data[$this->getPk()])) { // 完善编辑的时候验证唯一
                $map[$this->getPk()] = array('neq',$data[$this->getPk()]);#真正问题所在!
            }
            if($this->where($map)->find())
                return false;
            break;
        case 'regex':
        default:    // 默认使用正则验证 可以使用验证类中定义的验证名称
            // 检查附加规则
            return $this->regex($data[$val[0]],$val[1]);
    }
    return true;
}

不太清楚为什么程序在验证字段是否唯一的时候为什么要加入这段

 if(!empty($data[$this->getPk()])) { // 完善编辑的时候验证唯一
                $map[$this->getPk()] = array('neq',$data[$this->getPk()]);#问题所在!
            }
if($this->where($map)->find())
                return false;

$this->getPk() 函数是得到当前要判断的字段所在表的主键名称(注册时影响的表即为 ff_user,主键为 user_id。在thinkphp 中也有该函数)。如果存在,那么就用 'neq', 也即不等于。这里需要出现黑人问号?。等于说注册的时候我传入一个字段user_id就可以做一些事情了。例如下图

如果已经注册了一个user_name=myndtt并且user_id=2的用户,那么这样就完全绕过了字段验证。或者只需要传入user_id这个字段就可以绕过了。字段验证完以后没问题就会更新数据库了。例如下图(这里没有传入 user_name, user_email 等字段,仅仅传入了 user_id 和密码),那么程序就会对user_id对应的用户进行密码更改。

同时网站可以通过user_id来遍历得到注册用户的user_name。可以检测 user_id 是否存在。如

总之就可以利用user_id来更改ff_user表中的许多字段。

接着回到最早的post函数

if($info){#得到注册积分
   //注册积分
   if(C('user_register_score')){
      D('Score')->ff_user_score($info['user_id'], 2, intval(C('user_register_score')));
   }
   //推广积分
   if($info['user_pid'] && C('user_register_score_pid')){
       #echo '';#测试
      D('Score')->ff_user_score($info['user_pid'], 4, intval(C('user_register_score_pid')));
   }
   //json返回
   $data = array('id'=>$info['user_id'],'referer'=>cookie('ff_register_referer'));
   //欢迎邮件信息
   if( C('user_register_welcome') ){
      $content = str_replace(array('{username}','{sitename}','{time}'), array($info['user_name'],C('site_name'),time()), C('user_register_welcome'));
      D("Email")->send($info['user_email'], $info['user_name'], $info['user_name'].'您好,感谢您的注册', $content);
   }

如果user_id=自己的id话就可以无限注册给自己加分了。

那么问题来了,为什么不直接:加上一个 user_score 字段呢。如 post user_id=2&user_score=30000

回到post函数

$info = D("User")->ff_update($_POST);#执行完后表中user_id对应user_score为30000
        #var_dump($info);测试
        if($info){
            //注册积分
            if(C('user_register_score')){
                D('Score')->ff_user_score($info['user_id'], 2, intval(C('user_register_score')));#此时用给会计算ff_score表中对应id的score。以此为基础加上注册#的分数
            }

遗憾的是D('Score')->ff_user_score($info['user_id'], 2, intval(C('user_register_score')));会继续更新一次。在此中可以考虑时间竞争获得高额积分,否则就一次次发包,每次获得注册奖励的分数。

登入处

上述的更改用户密码,看似不能直接可以登入前台(登入需要邮箱),因为只能获得user_name

来到处理登入处的逻辑代码部分

public function loginpost(){    
    $user_id = D("User")->ff_login($_POST);#不好的现象
    if($user_id){
        $this->ajaxReturn($user_id, "登录成功", 200);
    }else{
        $this->ajaxReturn(0, D("User")->getError(), 500);
    }
}

进入ff_login函数

public function ff_login($post){
        $where = array();
        //用户名与邮箱登录
        if(filter_var($post['user_email'], FILTER_VALIDATE_EMAIL)){#如果user_email不符合#email的正则
            $where['user_email'] = array('eq', htmlspecialchars(trim($post['user_email'])));
        }else{
            $where['user_name'] = array('eq', #那么考虑用户输入的user_email可能是user_name
                                        htmlspecialchars(trim($post['user_email'])));
        }
        //查库
        $info = $this->field('user_id,user_name,user_pwd,user_email,user_status')->where($where)->find();
        if(!$info){

这种选择,考虑如果用户输入的不是邮箱就是用户名,经常在该一些 cms 中出现。可能在一种程度上方便了用户,但是也带来隐患。这里就是可以用 user_name 直接登入

危害总结

1、任意前台用户密码重置

2、任意用户刷分(影币)

3、用户其他数据的更改(头像链接,之类等)

修改

1、注册,登入处没必要用$_POST直接获取所有的 post 数据,多写几条代码,拿到自己想要的就好。

2、验证字段为空处的处理逻辑有问题,不空才检测,应当做限制。

3、验证具体字段唯一的时候何必去请求主键。

小结

像这种前台用户修改数据的地方往往是比较容易出现越权的地方。程序员为了方便,一次性获取所有用户 POST 的数据,没考虑用户在修改某一些字段的同时没其他字段数据是不是也会被修改,也很少考虑修改的数据是不是当前登入的用户。黑盒测试时,容易发现,白盒测试时,需要一段时间调试找到具体关键问题点。

原文发布于微信公众号 - 信安之路(xazlsec)

原文发表时间:2018-11-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏linux驱动个人学习

SMP多核启动

在 Linux系统中,对于多核的ARM芯片而言,在Biotron代码中,每个CPU都会识别自身ID,如果ID是0,则引导Bootloader和 Linux内核执...

28450
来自专栏BeJavaGod

如何限制用户在某一时间段多次访问接口

要知道,如今很多平台的接口都是可以同时被门户网站,手机端,移动浏览器访问,因为接口是通用的,而为了安全起见,有些接口都会设置一个门槛,那就是限制访问次数,也就是...

40260
来自专栏Java技术分享

“金三银四”招聘期又要到了,快来复习JAVA题!!

由于各操作系统(windows,liunx等)支持的指令集,不是完全一致的。就会让我们的程序在不同的操作系统上要执行不同程序代码。Java开发了适用于不同操作...

1.8K130
来自专栏coding

Linux笔记1

29630
来自专栏程序员的酒和故事

google/protobuf--VS2015编译、使用

本想用google的libphonenumber这个库来进行电话号相关功能,但是看到需要依赖protobuf,反正都是谷歌出品,那就顺便了解学习一下protob...

45460
来自专栏Ryan Miao

Git 工作流的正确打开方式

前言 一直在使用git做版本控制,也一直工作很顺利,直到和别人发生冲突的时候。这才注意到git 工作流并不是那么简单。比如,之前遇到的清理历史。百度到的资料很...

33760
来自专栏Golang语言社区

从websocket看go的应用

Go是互联网时代的通用编程语言。这样它就和命令行时代的C语言、图示界面时代的C++、以及互联网早期的Java语言等有不同的侧重。它强调保持自身的精巧和独立,从而...

38870
来自专栏Python数据科学

Python爬虫之模拟登录wechat

不知何时,微信已经成为我们不可缺少的一部分了,我们的社交圈、关注的新闻或是公众号、还有个人信息或是隐私都被绑定在了一起。既然它这么重要,如果我们可以利用爬虫模拟...

3.3K20
来自专栏贾老师の博客

【译】进程的内存剖析

18350
来自专栏非著名程序员

Android Studio你不知道的调试技巧

? 写代码不可避免有Bug,通常情况下除了日志最直接的调试手段就是debug;那么你的调试技术停留在哪一阶段呢?仅仅是下个断点单步执行吗?或者你知道 Eval...

322100

扫码关注云+社区

领取腾讯云代金券