jQuery-File-Upload-arbitrarily-file-upload-Vuln

-w914

在这看到的, 看到这个的时候有点疑惑这玩意不应该就是个前端的上传么, 怎么还会造成任意文件上传漏洞。 就去下了个源码看了下。

漏洞复现

https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.22.0

下载了代码后, 发现原来也带了后端处理上传的脚本。在server目录下。

在server/index.php中

1
2
require('UploadHandler.php');
$upload_handler = new UploadHandler();

实例化了UploadHandler类。
在UploadHandler的构造方法中

构造方法中, 定义了一些配置信息。
然后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function __construct($options = null, $initialize = true, $error_messages = null) {
$this->response = array();
$this->options = array(
'script_url' => $this->get_full_url().'/'.$this->basename($this->get_server_var('SCRIPT_NAME')),
'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/',
'upload_url' => $this->get_full_url().'/files/',
'input_stream' => 'php://input',
'user_dirs' => false,
'mkdir_mode' => 0755,
'param_name' => 'files',
.................
.................
if ($initialize) {
$this->initialize();
}

然后就调用了类中的initialize方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected function initialize() {
switch ($this->get_server_var('REQUEST_METHOD')) {
case 'OPTIONS':
case 'HEAD':
$this->head();
break;
case 'GET':
$this->get($this->options['print_response']);
break;
case 'PATCH':
case 'PUT':
case 'POST':
$this->post($this->options['print_response']);
break;
case 'DELETE':
$this->delete($this->options['print_response']);
break;
default:
$this->header('HTTP/1.1 405 Method Not Allowed');
}

根据对应的请求方式调用不同的方法。
大概get对应的是下载, post对应的是上传, delete对应的是删除操作。
但是在get和delete方法中, 由于都经过了basename方法的处理
所以只能删除files目录下的文件。

在post方法中

1
2
3
4
5
6
7
8
9
10
11
12
$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
);

获取到FILES变量中的数据后, 直接就调用了handle_file_upload方法

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
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($uploaded_file, $name, $size, $type, $error,
$index, $content_range);
$file->size = $this->fix_integer_overflow((int)$size);
$file->type = $type;
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);
}

在handle_file_upload方法中, 只要通过了validate方法, 那么就直接执行move_upload_file了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function validate($uploaded_file, $file, $error, $index) {
if ($error) {
$file->error = $this->get_error_message($error);
return false;
}
$content_length = $this->fix_integer_overflow(
(int)$this->get_server_var('CONTENT_LENGTH')
);
$post_max_size = $this->get_config_bytes(ini_get('post_max_size'));
if ($post_max_size && ($content_length > $post_max_size)) {
$file->error = $this->get_error_message('post_max_size');
return false;
}
if (!preg_match($this->options['accept_file_types'], $file->name)) {
$file->error = $this->get_error_message('accept_file_types');
return false;
}

这里通过或者配置变量数组中的accept_file_types来正则验证上传的文件名,
‘accept_file_types’ => ‘/.+$/i’,
配置文件中的accept_file_types为.+,
.代表着匹配任意字符 +1到多个字符, 所以是根本没有验证后缀的。
造成了任意文件上传。

1
2
$ curl -F "files=@yu.php" http://localhost/jQuery-File-Upload-9.22.0/server/php/index.php
{"files":[{"name":"yu.php","size":20,"type":"application\/octet-stream","url":"http:\/\/localhost\/jQuery-File-Upload-9.22.0\/server\/php\/files\/yu.php","deleteUrl":"http:\/\/localhost\/jQuery-File-Upload-9.22.0\/server\/php\/index.php?file=yu.php","deleteType":"DELETE"}]}

就能直接上传成功了,上传到了files目录中。 但是访问后发现脚本没有执行。
在files目录下 有着一个.htaccess

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
# To enable the Headers module, execute the following command and reload Apache:
# sudo a2enmod headers
# The following directives prevent the execution of script files
# in the context of the website.
# They also force the content-type application/octet-stream and
# force browsers to display a download dialog for non-image files.
SetHandler default-handler
ForceType application/octet-stream
Header set Content-Disposition attachment
# The following unsets the forced type and Content-Disposition headers
# for known image files:
<FilesMatch "(?i)\.(gif|jpe?g|png)$">
ForceType none
Header unset Content-Disposition
</FilesMatch>
# The following directive prevents browsers from MIME-sniffing the content-type.
# This is an important complement to the ForceType directive above:
Header set X-Content-Type-Options nosniff
# Uncomment the following lines to prevent unauthorized download of files:
#AuthName "Authorization required"
#AuthType Basic
#require valid-user

The directives ForceType and SetHandler are used to associated all the files in a given location (e.g., a particular directory) onto a particular MIME type or handler.

.htaccess配置了 在files目录下 强制由default-handler来处理所有文件m, 并且强制mime type为application/octet-stream, 使files目录下的脚本不会被执行。开发者也是因为配置了.htaccess的情况下, 以为是绝对的安全了, 所以才没有进行验证后缀。

这里虽然配置了.htaccess 但仍然有两个安全隐患
1: .htaccess只对apache有效, 而在纯nginx(非反向代理)中是无效的, 在jquery-upload的readme中 好像也并没有看到有说明使用nginx时的安全隐患。

-w630

2: 在apache 2.3.9以后, allowoverride默认为none

https://httpd.apache.org/docs/current/mod/core.html#allowoverride

AllowOverride Directive
Description: Types of directives that are allowed in .htaccess files
Syntax: AllowOverride All|None|directive-type [directive-type] …
Default: AllowOverride None (2.3.9 and later), AllowOverride All (2.3.8 and earlier)
Context: directory
Status: Core
Module: core

可以看到很清楚的说明了, 在apache 2.3.9及以后版本 allowoverride默认为none,
在2.3.8及之前版本allowoverride默认为all。
allowoverride指定了在.htaccess配置文件中, 可以覆盖掉主配置文件的指令。 当allowoverride为none时, .htaccess文件就失去了它的作用, 所以jquery-upload中对files目录所设置的拒绝脚本文件执行也就失效了。 导致了getshell。

修复

Jquery-file-upload在9.22.1版本中修复了该漏洞。
-w977
在实例化UploadHandler类的时候, 传递了一个数组进去。数组里面带了一个accept_file_types来验证后缀。
在UploadHandler类的构造方法中,

1
2
3
if ($options) {
$this->options = $options + $this->options;
}

把传递进来的数组和自身的配置变量数组用加号进行合并,
使用加号合并时 出现key冲突时 前面的数组对应的value会覆盖掉后面的。
所以这时
$this->options['accept_file_types']'/\.(gif|jpe?g|png)$/i'
在上传之前的validate方法中有通过获取accept_file_types来正则验证上传的文件名, 所以修复后就只能上传图片文件了。
-w965
修复后再上传脚本文件就失败了。

References

1: https://httpd.apache.org/docs/current/mod/core.html#allowoverride

2: https://github.com/blueimp/jQuery-File-Upload/blob/master/VULNERABILITIES.md