The Exploit
Requires authenticated author-level access or higher.
curl -i -X POST "http://TARGET/wp-admin/admin-ajax.php" \
-H "Cookie: wordpress_logged_in_XXXXXXXXXXXXXXXXXXXXXXXX" \
-F "action=supreme_upload_json" \
-F "[email protected];type=application/json"
The server responds with a JSON success payload and an upload path, for example:
{"success":true,"url":"http://TARGET/wp-content/uploads/supreme/shell.php"}. The attacker now has an uploaded file on disk that may be executed via a web request if the server allows .php files in uploads.
Why this still matters at admin: Author-level access is enough to reach the JSON upload endpoint in this plugin. In WordPress, author accounts are common on multi-author blogs and can be compromised by stolen sessions, weak passwords, or malicious insiders, making the upload vector a realistic escalation path to remote code execution.
What the Patch Did
Before:
if ( ! empty( $_FILES['file']['name'] ) ) {
$file = $_FILES['file'];
$filetype = wp_check_filetype( $file['name'], array( 'json' => 'application/json' ) );
if ( empty( $filetype['ext'] ) ) {
return new WP_Error( 'invalid_file', 'Only JSON uploads are allowed.' );
}
$uploaded = wp_handle_upload( $file, array( 'test_form' => false ) );
}
After:
if ( ! empty( $_FILES['file']['name'] ) ) {
$file = $_FILES['file'];
$file['name'] = sanitize_file_name( $file['name'] );
$filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'], array( 'json' => 'application/json' ) );
if ( empty( $filetype['ext'] ) || $filetype['ext'] !== 'json' ) {
return new WP_Error( 'invalid_file', 'Only JSON uploads are allowed.' );
}
$uploaded = wp_handle_upload( $file, array( 'test_form' => false ) );
}
The patch adds stricter upload validation by using WordPress’s wp_check_filetype_and_ext() and sanitizing the original filename with sanitize_file_name() before accepting it.
Root Cause
This is an unrestricted file upload bug (CWE-434) caused by improper file type validation. The attacker-controlled upload field file is passed into wp_check_filetype() using only the original filename. A double-extension payload like shell.php.json is accepted because the last extension is .json, even though the file can still be written to disk with .php semantics. The request crosses the trust boundary from user input to filesystem upload (wp_handle_upload) without sufficient enforcement of content-type and extension consistency.
Why It Works
The load-bearing fix is the switch from wp_check_filetype() to wp_check_filetype_and_ext() on the uploaded temporary file. wp_check_filetype() only inspects the filename, so shell.php.json appears valid as JSON. wp_check_filetype_and_ext() verifies the actual MIME/extension mapping against the uploaded temporary file contents, which prevents double-extension bypasses. sanitize_file_name() is a secondary hardening step that normalizes attacker-controlled filenames and removes dangerous characters, but the exploit would still work without it if the main filetype check remained weak.
Hardening Checklist
- Use
current_user_can('upload_files')or a tighter capability check before accepting uploads. - Validate uploads with
wp_check_filetype_and_ext( $_FILES['file']['tmp_name'], $_FILES['file']['name'], $mimes )instead ofwp_check_filetype(). - Normalize filenames with
sanitize_file_name()before passing them to upload handlers. - Restrict accepted upload extensions explicitly and verify
['ext']equals the allowed type (jsonin this case). - Keep file uploads out of executable directories or ensure web server configs disallow execution from
wp-content/uploads.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-13062