31 Jan 2019

Our Proactive Monitoring Caught an Authenticated Arbitrary File Upload Vulnerability Being Introduced in to a WordPress Plugin with 300,000+ Installs

With our proactive monitoring of changes made to WordPress plugins in the Plugin Directory to try to catch serious vulnerabilities we use software to flag potentially issues (you can check plugins in the same way using our Plugin Security Checker) and then we manually to check over the code. The second part of that can take a substantial amount of time, as while sometimes the code that runs before the potentially vulnerable code is limited and tightly woven, often it isn’t. That was the case with the code that leads to an authenticated arbitrary file upload vulnerability we found had being introduced in the plugin Meta Box, which has 300,000+ installs according to wordpress.org.

Due to the moderators of the WordPress Support Forum’s continued inappropriate behavior we are full disclosing vulnerabilities in protest until WordPress gets that situation cleaned up, so we are releasing this post and then only trying to notify the developer through the WordPress Support Forum. You can notify the developer of this issue on the forum as well. Hopefully the moderators will finally see the light and clean up their act soon, so these full disclosures will no longer be needed (we hope they end soon). You would think they would have already done that since a previously full disclosed vulnerability was quickly on hackers’ radar, but it appears those moderators have such disdain for the rest of the WordPress community that their continued ability to act inappropriate is more important that what is best for the rest of the community.

Technical Details

One of the changelog entries for version 4.16.0 of the plugin, which was released yesterday, is:

New feature: allow users to upload files to custom folders in file field.

That sounds like a minor change, but it has a big security impact it turns out.

Part of the change related to that was to add the function handle_upload_custom_dir() in the file /inc/fields/file.php. That will save arbitrary files to the website without any restrictions:

443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
public static function handle_upload_custom_dir( $file_id, $post_id, $field ) {
	// @codingStandardsIgnoreStart
	if ( ! isset( $_FILES[ $file_id ] ) ) {
		return;
	}
	$file = $_FILES[ $file_id ];
	if ( UPLOAD_ERR_OK !== $file['error'] || ! $file['tmp_name'] ) {
		return;
	}
	// @codingStandardsIgnoreEnd
 
	if ( ! file_exists( $field['upload_dir'] ) ) {
		wp_mkdir_p( $field['upload_dir'] );
	}
	if ( ! is_dir( $field['upload_dir'] ) || ! is_writable( $field['upload_dir'] ) ) {
		return;
	}
 
	$file_name = wp_unique_filename( $field['upload_dir'], basename( $file['name'] ) );
	$path      = trailingslashit( $field['upload_dir'] ) . $file_name;
	move_uploaded_file( $file['tmp_name'], $path );

That function will run when the function handle_upload() runs if “$field[‘upload_dir’]” exists, though as you can see that value gets passed from somewhere else:

262
263
protected static function handle_upload( $file_id, $post_id, $field ) {
	return $field['upload_dir'] ? self::handle_upload_custom_dir( $file_id, $post_id, $field ) : media_handle_upload( $file_id, $post_id );

That function in turn gets run by the function view():

212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
public static function value( $new, $old, $post_id, $field ) {
	$input = $field['file_input_name'];
 
	// @codingStandardsIgnoreLine
	if ( empty( $_FILES[ $input ] ) ) {
		return $new;
	}
 
	$new = array_filter( (array) $new );
 
	// Non-cloneable field.
	if ( ! $field['clone'] ) {
		$count = self::transform( $input );
		for ( $i = 0; $i <= $count; $i ++ ) {
			$attachment = self::handle_upload( "{$input}_{$i}", $post_id, $field );

When we came to that we had to change tracks try to figure how the vulnerable code would run, which required gaining an understanding of the plugin main functionality. Once we did that we found that a vulnerability existed.

The plugin provides an online generator for creating a new meta box that will be shown on the page to create a post or page. So a WordPress user at the Contributor level and above normally could access that meta box. If you create a meta box with a “HTML Image” input and set a custom folder for the uploads to be stored, the code above will run. With that, you can upload a .php file instead of an image as no checking of the file is done, as the proof of concept below shows.

Interestingly just a week ago someone brought up a concern that something like this might be possible, but at the time it doesn’t look like it was due it relying on WordPress code for handling the upload.

Proof of Concept

Add the following code to the active theme’s functions.php and add a .php file when creating a post as a user with the Contributor.

function your_prefix_get_meta_box( $meta_boxes ) {
	$prefix = 'prefix-';
 
	$meta_boxes[] = array(
		'id' => 'untitled',
		'title' => esc_html__( 'Untitled Metabox', 'metabox-online-generator' ),
		'post_types' => array('post', 'page' ),
		'context' => 'advanced',
		'priority' => 'default',
		'autosave' => 'false',
		'fields' => array(
			array(
				'id' => $prefix . 'image_1',
				'type' => 'image',
				'name' => esc_html__( 'Image Upload', 'metabox-online-generator' ),
				'upload_dir' => '../',
			),
		),
	);
 
	return $meta_boxes;
}
add_filter( 'rwmb_meta_boxes', 'your_prefix_get_meta_box' );

Concerned About The Security of the Plugins You Use?

When you are a paying customer of our service, you can suggest/vote for the WordPress plugins you use to receive a security review from us. You can start using the service for free when you sign up now. We also offer security reviews of WordPress plugins as a separate service.

Leave a Reply

Your email address will not be published.