Exploit-DB #10089
Description: WordPress Core < 2.8.5 - Unrestricted Arbitrary File Upload / Arbitrary PHP Code Execution
Type: webapps
Platform: php
Date: 2009-11-11
Author: Dawid Golunski

Exploit:
=============================================
- Release date: November 11th, 2009
- Discovered by: Dawid Golunski
- Severity: Moderately High
=============================================

I. VULNERABILITY
-------------------------
WordPress <= 2.8.5 Unrestricted File Upload Arbitrary PHP Code Execution

II. BACKGROUND
-------------------------
WordPress is a state-of-the-art publishing platform with a focus on  
aesthetics, web standards,
and  usability. WordPress is both free and priceless at the same time.  
More simply, WordPress is
what you use when you want to work with your blogging software, not  
fight it.

III. DESCRIPTION
-------------------------

Wordpress allows authorised users to add an attachment to a blog post.
It does not sanitize provided file properly before moving it to an  
uploads directory.

The part of the code responsible for uploading files looks as follows:

wp-admin/includes/file.php:
---[cut]---
line 217:
function wp_handle_upload( &$file, $overrides = false, $time = null ) {
---[cut]---
// All tests are on by default. Most can be turned off by  
$override[{test_name}] = false;
$test_form = true;
$test_size = true;

// If you override this, you must provide $ext and $type!!!!
$test_type = true;
$mimes = false;
---[cut]---

// A properly uploaded file will pass this test. There should be no  
reason to override this one.
if (! @ is_uploaded_file( $file['tmp_name'] ) )
         return $upload_error_handler( $file, __( 'Specified file  
failed upload test.' ));

// A correct MIME type will pass this test. Override $mimes or use the  
upload_mimes filter.
if ( $test_type ) {
         $wp_filetype = wp_check_filetype( $file['name'], $mimes );

         extract( $wp_filetype );

         if ( ( !$type || !$ext ) && ! 
current_user_can( 'unfiltered_upload' ) )
                 return $upload_error_handler( $file,
                                               __( 'File type does not meet security guidelines. Try  
another.' ));

         if ( !$ext )
                 $ext = ltrim(strrchr($file['name'], '.'), '.');

         if ( !$type )
                 $type = $file['type'];
} else {
         $type = '';
}

// A writable uploads dir will pass this test. Again, there's no point  
overriding this one.
if ( ! ( ( $uploads = wp_upload_dir($time) ) && false ===  
$uploads['error'] ) )
         return $upload_error_handler( $file, $uploads['error'] );

$filename = wp_unique_filename( $uploads['path'], $file['name'],  
$unique_filename_callback );

// Move the file to the uploads dir
$new_file = $uploads['path'] . "/$filename";
if ( false === @ move_uploaded_file( $file['tmp_name'], $new_file ) ) {
         return $upload_error_handler( $file,
                   sprintf( __('The uploaded file could not be moved to %s.' ),  
$uploads['path'] ) );
}
---[cut ]---

 From the above code we can see that provided filename gets checked  
with:
$wp_filetype = wp_check_filetype( $file['name'], $mimes );

Here is how the wp_check_filetype() function looks like:

wp-includes/functions.php:
---[cut]---
line 2228:

function wp_check_filetype( $filename, $mimes = null ) {
         // Accepted MIME types are set here as PCRE unless provided.
         $mimes = ( is_array( $mimes ) ) ? $mimes :  
apply_filters( 'upload_mimes', array(
                 'jpg|jpeg|jpe' => 'image/jpeg',
                 'gif' => 'image/gif',
                 'png' => 'image/png',
                 'bmp' => 'image/bmp',
                 'tif|tiff' => 'image/tiff',
                 'ico' => 'image/x-icon',
                 'asf|asx|wax|wmv|wmx' => 'video/asf',
                 'avi' => 'video/avi',
                                
                                ---[cut, more mime types]---
line 2279:

         $type = false;
         $ext = false;

         foreach ( $mimes as $ext_preg => $mime_match ) {
                 $ext_preg = '!\.(' . $ext_preg . ')$!i';
                 if ( preg_match( $ext_preg, $filename,  
$ext_matches ) ) {
                         $type = $mime_match;
                         $ext = $ext_matches[1];
                         break;
                 }
         }

         return compact( 'ext', 'type' );
}

We can see that type of the file gets set to a predefined MIME type  
that matches supplied
extension, and that the extension is obtained from a regexp that  
matches a mime ext. string after
the LAST dot.
If extension is not on the list $type and $ext will be set to FALSE  
and wordpress will
produce an error ("File type does not meet security guidelines. Try  
another").

Let's look at the other check that is performed on the filename before  
a file gets uploaded,
that is a call to the following function:
$filename = wp_unique_filename( $uploads['path'], $file['name'],  
$unique_filename_callback );


wp-includes/functions.php:
line 2096:
function wp_unique_filename( $dir, $filename,  
$unique_filename_callback = null ) {
         // sanitize the file name before we begin processing
         $filename = sanitize_file_name($filename);

         ---[cut, code that only matters if uploaded file already  
exists]---
line 2126:
                return $filename;
}

To have a complete view on file sanitization performed by wordpress we  
need to look into the
sanitize_file_name() function:

wp-includes/formatting.php:
line 601:
function sanitize_file_name( $filename ) {
         $filename_raw = $filename;
         $special_chars = array("?", "[", "]", "/", "\\", "=", "<",  
">", ":", ";", ",", "'", "\"",
                                "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}", chr(0));
         $special_chars = apply_filters('sanitize_file_name_chars',  
$special_chars, $filename_raw);
         $filename = str_replace($special_chars, '', $filename);
         $filename = preg_replace('/[\s-]+/