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