This is a side door left unlocked on a storefront that only exists if you installed this exact plugin
CVE-2021-24931 is an unauthenticated SQL injection in the *Secure Copy Content Protection and Content Locking* WordPress plugin. Affected versions are all releases before 2.8.2; the bug sits in the ays_sccp_results_export_file AJAX action, where the sccp_id parameter is fed into a SQL query without proper sanitization. Because the action is reachable through /wp-admin/admin-ajax.php for unauthenticated users, an external attacker can hit it directly over HTTP and pull or tamper with WordPress database content.
The raw 9.8 score is directionally understandable on a lab bench: no auth, no user click, database compromise potential. In real fleets, though, this is not a universal WordPress bug; it only matters on sites running this specific plugin, and even then the practical target set is public-facing WordPress estates rather than your whole enterprise. That trims it down from *internet-on-fire critical* to a very solid HIGH—still patch-worthy fast because public PoC, scanner support, high EPSS, and current attack telemetry all keep the risk elevated.
4 steps from start to impact.
Identify a site running the plugin
secure-copy-content-protection is installed. Common tooling is WPScan, passive plugin enumeration, or a nuclei template reference set that specifically targets this CVE. Once the plugin is confirmed, the attacker knows a public AJAX route may be reachable without login.- Target runs WordPress
- The vulnerable plugin is installed in a version older than
2.8.2 - The site is reachable over HTTP/HTTPS
- This is a plugin-specific flaw, not a WordPress core issue
- Many enterprises do not run this plugin at all
- Passive fingerprinting can fail if plugin paths are hidden or aggressively cached
Reach the unauthenticated AJAX handler
/wp-admin/admin-ajax.php?action=ays_sccp_results_export_file. Because this action is exposed to unauthenticated users, the attacker does not need a WordPress account, session, or CSRF setup. Weaponization is simple with curl, browser requests, or sqlmap pointed at the vulnerable parameter.- The site's
admin-ajax.phpendpoint is externally reachable - No upstream control blocks the request shape
- Some WAFs and managed WordPress front ends will flag obvious SQLi syntax
- Rate limits or bot controls can slow bulk exploitation
/wp-admin/admin-ajax.php with action=ays_sccp_results_export_file and malformed sccp_id values.Exploit sccp_id with SQL injection
sccp_id parameter is concatenated into a database query without safe preparation. Public references include a WPScan proof of concept and a ProjectDiscovery nuclei template that uses time-based SQLi for validation; operators can move from that to sqlmap for enumeration and extraction. In the common WordPress deployment model, the app's DB user has broad rights over the WordPress schema, so the attacker can often read user hashes, emails, and site configuration.- Backend database executes the crafted payload
- WordPress DB credentials used by the app have normal schema access
- Blind/time-based extraction is slower than a clean UNION path
- Custom DB hardening or reduced grants can limit write impact
- WAF normalization may break noisy payloads before they land
Turn database access into tenant compromise
wp_users, steal password hashes, enumerate salts and options, or alter application data. Depending on DB permissions and local hardening, they may pivot into admin takeover by changing credentials or planting data that supports later code execution through the CMS. The practical impact is typically full compromise of that WordPress site and its content store, not broad domain-wide enterprise takeover.- Useful data exists in the WordPress database
- The attacker can successfully extract or modify rows
- Impact is mostly confined to the affected WordPress tenant
- EDR does not help much until the attacker moves beyond SQLi into post-exploitation
- Strong credential hygiene can blunt the value of dumped password hashes
/wp-admin/admin-ajax.php where action=ays_sccp_results_export_file, or restrict that action to trusted admin source ranges if the workflow is genuinely needed. Because there is active exploitation evidence, deploy this within hours, not on a normal 30-day HIGH cycle./wp-admin/admin-ajax.php requests with action=ays_sccp_results_export_file, array-style sccp_id[], UNION, SLEEP(, or abnormally repeated export attempts. Start the hunt immediately and keep it active until patch coverage is complete.2.8.2 or later and verify no stragglers remain in staging, DR, or forgotten marketing sites. Because current exploitation exists, treat patching as accelerated and begin within hours, not at the edge of the usual HIGH timeline.- EDR on the web server does not reliably stop SQLi at the HTTP layer; it only helps if the intrusion turns into post-exploitation on the host.
- Hiding
wp-content/plugins/paths is not a control. It may slow passive fingerprinting, but the vulnerable endpoint can still be probed directly. - MFA for WordPress admins is good hygiene but irrelevant to the initial exploit path, because this bug is reachable without authentication.
The supporting signals.
| In-the-wild status | Not in CISA KEV, but this is not dormant. CrowdSec currently reports 636 exploiting IPs and steady automated scanning against the vulnerable AJAX route. |
|---|---|
| Proof-of-concept availability | Public PoC exists in the WPScan advisory. NVD also links a Packet Storm exploit reference, and ProjectDiscovery ships a nuclei template. |
| EPSS | 0.7216 with roughly 98.8th percentile exposure pressure, which is very high for a WordPress plugin CVE. |
| KEV status | Absent from the CISA Known Exploited Vulnerabilities catalog at time of review. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H correctly captures the *technical* ease and impact, but it ignores the biggest real-world filter: only sites running this specific plugin are in play. |
| Affected versions | All plugin versions before 2.8.2 are affected; NVD models this as a CPE range ending before 2.8.2. |
| Fixed versions | The flaw is fixed in 2.8.2. The public plugin has since moved far beyond that; WordPress.org currently lists 5.1.6. |
| Exposure population | WordPress.org currently shows 20,000+ active installations. That is meaningful internet exposure, but still far smaller than a mainstream plugin or WordPress core issue. |
| Disclosure timeline | WPScan lists public publication on 2021-11-08; NVD published the CVE on 2021-12-06. |
| Researcher / submitter | Credited to Krzysztof Zając, per WPScan/OpenCVE record data. |
Sources.
Why this verdict
- Unauthenticated remote path: no login, no click, no prior foothold; the attacker can hit
admin-ajax.phpdirectly from the internet, which preserves a high baseline. - Public weaponization exists: WPScan published a concrete PoC, NVD links exploit references, and ProjectDiscovery maintains a ready-made
nucleicheck. This is easy to operationalize withcurl,sqlmap, or scanner automation. - Observed attack pressure is real: despite no KEV listing, CrowdSec reports active exploitation telemetry and hundreds of exploiting IPs, so this is not theoretical shelfware.
- Downward pressure from exposure population: this is a plugin-specific bug. The attacker needs a site that actually runs *Secure Copy Content Protection and Content Locking* below
2.8.2, which sharply narrows the target fraction versus a core WordPress or mass-market plugin issue. - Blast radius is usually tenant-level: compromise is often severe for the affected WordPress site, but it does not inherently imply domain-wide enterprise compromise unless that site is already a privileged hub.
Why not higher?
I am not calling this CRITICAL because the main friction is population reachability, not exploit syntax. A 9.8 score assumes every network-reachable instance matters equally; in production, only a subset of public WordPress sites with this exact plugin installed are exposed, and the likely blast radius is usually one web tenant at a time rather than immediate estate-wide compromise.
Why not lower?
I am not dropping this to MEDIUM because the attack does not require credentials, internal access, user interaction, or chaining with another bug. Add the public PoC, scanner support, and active opportunistic exploitation telemetry, and this is well above backlog hygiene.
Crowdsourced verification payload.
Run this on the target WordPress host or in a build/CI artifact where the plugin files are present. Invoke it as bash check-cve-2021-24931.sh /var/www/html and use a path to the WordPress root; read access only to the plugin directory is sufficient.
#!/usr/bin/env bash
# check-cve-2021-24931.sh
# Detects whether the Secure Copy Content Protection and Content Locking plugin
# is vulnerable to CVE-2021-24931 based on installed version.
#
# Usage: bash check-cve-2021-24931.sh /path/to/wordpress
# Exit codes:
# 0 = PATCHED
# 1 = VULNERABLE
# 2 = UNKNOWN (not installed / unreadable / cannot determine)
set -u
TARGET_VERSION="2.8.2"
WP_ROOT="${1:-}"
PLUGIN_DIR_REL="wp-content/plugins/secure-copy-content-protection"
if [[ -z "$WP_ROOT" ]]; then
echo "UNKNOWN - usage: $0 /path/to/wordpress"
exit 2
fi
PLUGIN_DIR="$WP_ROOT/$PLUGIN_DIR_REL"
if [[ ! -d "$PLUGIN_DIR" ]]; then
echo "UNKNOWN - plugin directory not found: $PLUGIN_DIR"
exit 2
fi
# Find the plugin header file by looking for the plugin name or a Version header.
PLUGIN_FILE=""
while IFS= read -r -d '' file; do
if grep -qi "Plugin Name:.*Secure Copy Content Protection" "$file" 2>/dev/null; then
PLUGIN_FILE="$file"
break
fi
done < <(find "$PLUGIN_DIR" -maxdepth 2 -type f -name '*.php' -print0 2>/dev/null)
if [[ -z "$PLUGIN_FILE" ]]; then
# Fallback: grab the first PHP file with a Version header.
while IFS= read -r -d '' file; do
if grep -qi '^\s*Version:' "$file" 2>/dev/null; then
PLUGIN_FILE="$file"
break
fi
done < <(find "$PLUGIN_DIR" -maxdepth 2 -type f -name '*.php' -print0 2>/dev/null)
fi
if [[ -z "$PLUGIN_FILE" || ! -r "$PLUGIN_FILE" ]]; then
echo "UNKNOWN - could not locate readable plugin header file"
exit 2
fi
INSTALLED_VERSION=$(awk -F': *' 'BEGIN{IGNORECASE=1} /^\s*Version:/ {print $2; exit}' "$PLUGIN_FILE" | tr -d '\r')
if [[ -z "$INSTALLED_VERSION" ]]; then
echo "UNKNOWN - could not parse version from $PLUGIN_FILE"
exit 2
fi
version_lt() {
# returns 0 if $1 < $2
[[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" != "$2" && "$1" != "$2" ]]
}
if version_lt "$INSTALLED_VERSION" "$TARGET_VERSION"; then
echo "VULNERABLE - installed version $INSTALLED_VERSION is older than fixed version $TARGET_VERSION"
exit 1
fi
if [[ "$INSTALLED_VERSION" == "$TARGET_VERSION" || "$(printf '%s\n%s\n' "$INSTALLED_VERSION" "$TARGET_VERSION" | sort -V | tail -n1)" == "$INSTALLED_VERSION" ]]; then
echo "PATCHED - installed version $INSTALLED_VERSION is at or above fixed version $TARGET_VERSION"
exit 0
fi
echo "UNKNOWN - installed version $INSTALLED_VERSION could not be compared reliably"
exit 2