The Exploit
An unauthenticated attacker with network access and an IP whose reverse DNS resolves to a domain containing cleantalk.org can call the CleanTalk remote API and install plugins without a token.
curl -i -X POST 'https://TARGET/wp-admin/admin-ajax.php?action=apbct_remote_call' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'action=install_plugin&plugin_slug=hello-dolly'
The vulnerable host responds as if the remote call were trusted, typically returning a success payload from the CleanTalk handler. The side effect is that the plugin install path is executed without normal authentication, allowing an attacker to install and activate arbitrary plugins.
What the Patch Did
Before:
if (strpos(Helper::ipResolve(Helper::ipGet()), 'cleantalk.org') !== false) {
return true;
}
...
return self::checkWithoutToken();
After:
if (in_array(Helper::ipResolve(Helper::ipGet('remote_addr')), $rc_servers, true)) {
return true;
}
...
return (self::checkWithoutToken() && self::isAllowedWithoutToken($action));
The patch replaced a fragile reverse-DNS string match with an exact allowlist check against trusted CleanTalk server IPs, and it added an action-level authorization gate using self::isAllowedWithoutToken($action).
Root Cause
This is an authorization bypass (CWE-862) caused by trusting attacker-controlled network identity. checkWithoutToken() used Helper::ipGet() and Helper::ipResolve() to resolve the client IP to a hostname, then accepted any hostname containing cleantalk.org via strpos(...) !== false. That allowed an attacker to cross the trust boundary: the remote request source IP was treated as if it belonged to CleanTalk just because its reverse DNS string contained cleantalk.org. The resulting token bypass was then applied to all remote call actions, so a request carrying action=install_plugin could reach the plugin installation path without proper authorization.
Why It Works
The single load-bearing fix is the added self::isAllowedWithoutToken($action) check. Without it, the IP allowlist alone would still permit a valid CleanTalk callback to invoke any action, including plugin install or activate. The in_array(..., $rc_servers, true) line hardens the source validation by dropping reverse DNS string matching; the explicit action whitelist prevents the bypass from being used for dangerous operations. Both are needed, but the action-level restriction is what stops arbitrary plugin installation once the remote call is accepted.
Hardening Checklist
- use
current_user_can('install_plugins')/current_user_can('activate_plugins')for any plugin install or activation endpoint. - do not trust
gethostbyaddr()/ reverse DNS for authentication; use exact IP allowlists within_array($ip, $allowed_ips, true)or signed requests instead. - keep a strict bypass whitelist: allow only specific actions with
in_array($action, $allowed_actions, true)before skipping token checks. - require
wp_verify_nonce()for authenticated admin operations and avoid anonymous remote calls unless they are explicitly limited. - source remote client IP from
$_SERVER['REMOTE_ADDR']rather than forwarded headers when making access decisions.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-10542