0x00
安装feifeicms4.0.18(建议直接安装在WWW目录下),具体代码,在我的资源里面,可免费下载
使用工具,phpstorm(别问,问就是喜欢它的界面)
0x01
漏洞代码所在
1
| \Lib\Lib\Action\Home\UserAction.class.php
|
出现问题的代码段在198-225
行
代码如下,简单分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public function post(){ $info = D("User")->ff_update($_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')){ 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
不知道它的用途,所以跟进看看,如果是使用phpstorm,可以直接使用ctrl+shift+F,搜索function ff_update,
\Lib\Lib\Model\AdsModel.class.php
,但是建议选择
\Lib\Lib\Model\UserModel.class.php
,因为这里本就是关于注册的内容,在回溯函数的时候,最好是找对应的代码
0x02
继续查看ff_update函数的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| // 新增或更新 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函数,因为不知道它是干嘛的,它又在最开始,继续使用上面的方法回溯,看了前面几个,一方面是因为没看见它的参数,另一方面是觉得它内容不符合关于注册的东西,所以我果断选择了下边这个函数
0x03
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| 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; }
|
简单看看,大概就是传入参数,然后进行判断,getPk
函数在这里没什么实际意思,大概就是不能为空,然后得到当前要判断的字段所在表的主键名称,autoCheckToken
是关于令牌验证的,这个,我还没学到,所以暂定为没问题,重心也不在这里。继续往下,可以跟进 autoValidation
函数查看程序如何对数据进行验证,在同一个文件里面
0x04
\Lib\ThinkPHP\Lib\Think\Core\Model.class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| 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; }
|
这段代码注释比较多,所以,不用我们自己怎么去分析,问题在于_validationField
函数,它虽然确实实在验证,但,这里看不出来它到底是怎么验证的,所以继续回溯吧,发现在同一个文件里
0x05
先看看autoValidation
函数验证了些什么参数吧
1 2 3 4 5 6 7 8
| protected $_validate = array( array('forum_vcode','require','请输入验证码!',1,'',1), array('forum_vcode','validate_vcode','验证码错误!',1,'callback',1), array('forum_uid','validate_uid','您还没有登录!',1,'callback',1), array('forum_cookie','validate_cookie','您已经评论过了!',2,'callback',1), array('forum_sid','require','您没有指定模型ID!',1,'',1), array('forum_content','require','请填写评论内容!',1), );
|
这些都是正常注册的,找不出漏洞,所以失败,回到刚刚的\Lib\ThinkPHP\Lib\Think\CoreModel.class.php
继续看
0x07
发现default
的部分:if(isset($data[$val[0]]))
只要传入的数据为空就不必进入检测了,这样会带来问题
那就是咯,这样不就出问题了吗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| 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; }
|
$this->getPk()
函数是得到当前要判断的字段所在表的主键名称(注册时影响的表即为 ff_user,主键为 user_id,在thinkphp 中也有该函数)。如果存在,那么就用 ‘neq’, 也即不等于。但是,它这里,都没什么验证啊,比如是验证是不是当前该用户,换句话说,就是没有限制访问其他id的页面,等于说注册完以后,随便传入一个字段user_id
就可以看其他用户了哦?只要这个id存在。
0x08
(原本是还有一个注册界面的越权的,但是没有发现相应的参数,只好作废)
大概原理:如果已经注册了一个user_name=test1
并且user_id=2
的用户,那么可以尝试绕过了字段验证。或者只需要传入user_id
这个字段就可以绕过了。字段验证完以后数据库会自动更新。(这里没有传入 user_name, user_email 等字段,仅仅传入了 user_id 和密码),那么程序就会对user_id
对应的用户进行密码更改。
同时网站可以通过user_id
来遍历得到注册用户的user_name
。可以检测 user_id 是否存在(水平越权)
又可以访问test2