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' ); |