jQuery-File-Upload-arbitrarily-file-upload-Vuln
在這看到的, 看到這個的時候有點疑惑這玩意不應該就是個前端的上傳麼, 怎麼還會造成任意檔案上傳漏洞。 就去下了個原始碼看了下。
漏洞復現
jQuery-File-Upload/releases/tag/v9.22.0" target="_blank" rel="nofollow,noindex">https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.22.0
下載了程式碼後, 發現原來也帶了後端處理上傳的指令碼。在server目錄下。
在server/index.php中
require('UploadHandler.php'); $upload_handler = new UploadHandler();
例項化了UploadHandler類。
在UploadHandler的構造方法中
構造方法中, 定義了一些配置資訊。
然後
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方法。
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方法中
$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方法
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了。
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到多個字元, 所以是根本沒有驗證字尾的。
造成了任意檔案上傳。
$ curl -F "[email protected]" 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
# 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時的安全隱患。
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版本中修復了該漏洞。

在例項化UploadHandler類的時候, 傳遞了一個數組進去。數組裡面帶了一個accept_file_types來驗證字尾。
在UploadHandler類的構造方法中,
if ($options) { $this->options = $options + $this->options; }
把傳遞進來的陣列和自身的配置變數陣列用加號進行合併,
使用加號合併時 出現key衝突時 前面的陣列對應的value會覆蓋掉後面的。
所以這時
$this->options['accept_file_types']
為
'/\.(gif|jpe?g|png)$/i'
在上傳之前的validate方法中有通過獲取accept_file_types來正則驗證上傳的檔名, 所以修復後就只能上傳圖片檔案了。

修復後再上傳指令碼檔案就失敗了。