08 Aug

Arbitrary File Upload Vulnerability Being Exploited in Current Version of Ultimate Member

The WordPress plugin Ultimate Member was recently brought on to our radar after it had been run through our Plugin Security Checker and that tool had identified a possible vulnerability in it. We happened to take a look into that as part of continued effort to improve the results coming from that tool. We confirmed that there was a vulnerability and notified the developer. The developer responded that they would fix that as soon as possible, but it has been nearly month and that hasn’t happened. In line with our disclosure policy we are scheduled to be disclosing that vulnerability on Friday. Thankfully that vulnerability isn’t something that is likely to be exploited in an untargeted hack, but there is another vulnerability that is presently being exploited in the current version, 2.0.21, of the plugin.

Yesterday we were contacted about a thread on the WordPress Support Forum discussing that possibility. In that thread the developer responded more than a day ago with:

We’ve overhauled our files upload and increased security, the update will be live very soon.
Please make sure to update to the latest version when it will be available.

There still hasn’t been a new version released.

When we went to look into that, one of the things we found was that there are a couple of files in the plugin that contain upload functionality that don’t seem to actually be used by the plugin. It isn’t clear what is going on there since they don’t seem to have been used in the first version they were introduced in either.

We also found that trying to follow the other upload functionality was somewhat confusing and so while we came close to understanding what might at issue, we didn’t fully crack things yesterday.

In further looking today we ran across another thread that contains several replies from today that add more detail on the hacking side that we could then confirm in the code.

The non-technical explanation for what is going on is that the plugin’s functionality for uploading images does not contain code that would fully restrict uploading malicious files as long as you are able to cause some of the code to see them as image files. Those malicious files get added to directories inside of the directory /wp-content/uploads/ultimatemember/temp/. While those directories and file names have randomized names it can be possible to determine them in certain circumstances and then hackers can take further action on the website through the files.

We are in the process of contacting the developer about the situation to see if that might speed up them releasing a fix.

One quick temporary solution to this is to disable the image upload functionality, which can be done by adding the following lines

			$ret['error'] = __('Functionality disabled');
			exit(json_encode($ret));

directly below the line

		function ajax_image_upload() {

in the file /includes/core/class-files.php.

Since this vulnerability is being exploited, we have made this vulnerability details post public (unlike most of them that are limited to our customers) and we are also adding the vulnerability to the free data that comes with our service’s companion plugin, which it would probably be a good idea to be using even if you don’t use our service since it will warn about just this type of situation.

If you need a website using this plugin cleaned up, our service for cleaning up a hacked WordPress website at our main website currently includes a free lifetime subscription to this service.

Wordfence Missed It

Partly, maybe largely, based on false claims made by the makers of the Wordfence Security plugin many people believe that the plugin is much more capable than it truly is. In this case it failed to stop the hack or even detect the after effect as indicated by one of the commentators in the first thread:

What monitor did you use? I had WordFence and it didn’t catch it.

We have personally been brought in to clean up many hacked websites where it either failed to protect the website and or it failed to detect the result of the hack afterwards.

The Underlying Code

The image upload functionality is handled through the aforementioned function ajax_image_upload(). In that function the function check_image_upload() checks the image and if there is an error stops the rest of the upload process from happening:

1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
			$error = UM()->files()->check_image_upload( $temp, $id );
			if ( $error ){
 
				$ret['error'] = $error;
 
			} else {
				$file = "stream_photo_".md5($file)."_".uniqid().".".$ext;
				$ret[ ] = UM()->files()->new_image_upload_temp( $temp, $file, UM()->options()->get('image_compression') );
 
			}
 
		}
 
	} else {
		$ret['error'] = __('A theme or plugin compatibility issue','ultimate-member');
	}
	echo json_encode($ret);
	exit;
}

At the beginning of the function check_image_upload() it calls the function get_image_data():

592
593
594
595
function check_image_upload( $file, $field ) {
	$error = null;
 
	$fileinfo = $this->get_image_data( $file );

That function in turn attempts to check for an invalid image and determine some information about the image:

556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
function get_image_data( $file ) {
 
	$array['size'] = filesize( $file );
 
	$array['image'] = @getimagesize( $file );
 
	if ( $array['image'] > 0 ) {
 
		$array['invalid_image'] = false;
 
		list($width, $height, $type, $attr) = @getimagesize( $file );
 
		$array['width'] = $width;
		$array['height'] = $height;
		$array['ratio'] = $width / $height;
 
		$array['extension'] = $this->get_extension_by_mime_type( $array['image']['mime'] );
 
	} else {
 
		$array['invalid_image'] = true;
 
	}
 
	return $array;
}

There are a couple of important issues with that though. The function getimagesize() is used there to determine if there is valid image, but the documentation for it states:

Caution
This function expects filename to be a valid image file. If a non-image file is supplied, it may be incorrectly detected as an image and the function will return successfully, but the array may contain nonsensical values.

Do not use getimagesize() to check that a given file is a valid image. Use a purpose-built solution such as the Fileinfo extension instead.

The other issue is the use MIME type to determine the extension of the file, since that is user specified and does not have to be the same as the actual file extension of the file.

Those two issues can be combined to allow a file with say a .php extension to be treated by that code as an image file. Based on part of a comment in the second thread that is in fact the type of file the hacker is uploading:

The files are spoofed gif images. So the mime-type will detect as gif. But then have php embedded in them. When pushed through the php processor the gif parts are passed through to the browser just like html in the file would be and showing up as garbage on the screen, and then the php is executed behind the scenes once it is encountered.

Getting back to the function check_image_upload() it uses the potentially inaccurate information from get_image_data() to check if there is an error:

668
669
670
671
672
673
674
675
676
677
678
679
680
681
	if ( $fileinfo['invalid_image'] == true ) {
		$error = sprintf(__('Your image is invalid or too large!','ultimate-member') );
	} elseif ( isset( $data['allowed_types'] ) && !$this->in_array( $fileinfo['extension'], $data['allowed_types'] ) ) {
		$error = ( isset( $data['extension_error'] ) && !empty( $data['extension_error'] ) ) ? $data['extension_error'] : 'not allowed';
	} elseif ( isset($data['min_size']) & ( $fileinfo['size'] < $data['min_size'] ) ) {
		$error = $data['min_size_error'];
	} elseif ( isset($data['min_width']) && ( $fileinfo['width'] < $data['min_width'] ) ) {
		$error = sprintf(__('Your photo is too small. It must be at least %spx wide.','ultimate-member'), $data['min_width']);
	} elseif ( isset($data['min_height']) && ( $fileinfo['height'] < $data['min_height'] ) ) {
		$error = sprintf(__('Your photo is too small. It must be at least %spx wide.','ultimate-member'), $data['min_height']);
	}
 
	return $error;
}

Since that can be bypassed in the fashion the hacker is doing the rest of the upload process then runs.

PHP’s Built-in Temp Directory

One of other element that seems worth mention for the programming set relates to another part of the comment we already quoted about the file being uploaded, they also wrote:

I’d recommend to the programmers in this case, if they are hell bent on using the ‘uploads’ folder as a ‘temp’ directory to ensure that they have an empty index.html file in the temp directory to help stop this attack vector.

And I would recommend to all wordpress users to disable/block php from running in the uploads folder(as above), because it’s not only these programmers that have decided that the uploads folder is a great place to use for general plugin data storage.

I’d go further and propose to all plugin coders that they stop this practice and instead create/support a non-web-accessible directory for such purposes which completely removes the attack vector in its entirety.

PHP actually has built-in functionality for handling temporary files, more can found in the documentation for the function tmpfile().

Leave a Reply

Your email address will not be published. Required fields are marked *