Abstract. Login LockDown is a WordPress plugin with more than 100,000 active installations. Among its protections, it can add a built-in math CAPTCHA to the login form. Version 2.18 verifies that CAPTCHA in a way that defeats it. The plugin sends the expected answer to the browser as a client-side token, then validates the login by recomputing a hash over two values that both arrive in the same request. Nothing binds the answer/token pair to server-side state, so it survives replay without limit, and the anti-automation the CAPTCHA promises quietly evaporates. What follows is the root cause and a defensive guide for site owners and developers. Under coordinated disclosure, the full working proof of concept stays private until a fix ships.
Keywords: Login LockDown, WordPress, CAPTCHA bypass, capture-replay, CWE-294, CWE-804, security-control bypass, anti-automation.
1. Introduction
Brute-force and credential-stuffing are the steadiest pressure any WordPress login endpoint faces. Plugins like Login LockDown raise the cost of automated guessing with IP lockouts and, optionally, a CAPTCHA on the login form and the WooCommerce login.
A protective control that can be bypassed is worse than one you know is absent. An administrator who turns on the built-in CAPTCHA assumes automated login attempts now hit a human-verification step. If that step can be opened once and replayed forever, the assumption is false, and the brute-force surface they believed closed is open again without any warning.
The issue sits in the built-in math CAPTCHA option of Login LockDown 2.18, the current release at the time of testing. It is gated on a non-default setting: the administrator has to select the built-in CAPTCHA (captcha = builtin). The IP-lockout feature was reviewed separately and holds up.
2. Affected component
| Plugin | Login LockDown — slug login-lockdown |
| Tested version | 2.18 (current/latest at time of testing) |
| Active installations | 100,000+ |
| Vulnerability type | Security-control bypass — insufficient CAPTCHA verification (capture-replay) |
| CWE | CWE-804 (Guessable CAPTCHA), CWE-294 (Authentication Bypass by Capture-replay); parent CWE-287 |
| Required privilege | None — unauthenticated, remote |
| Precondition | Admin has set the CAPTCHA option to the built-in math CAPTCHA (captcha = builtin; not the default) |
| CVSS 3.1 | ~5.3 (Medium) AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N |
| Status | Fresh — unpatched; confirmed in code and reproduced in an isolated localhost lab on the current version |
3. Technical analysis
The built-in math CAPTCHA asks the visitor to solve a small sum. Everything turns on where the expected answer lives, and Login LockDown keeps it nowhere on the server. There is no session entry and no transient. For each challenge the login form emits two fields instead:
loginlockdown_captcha[<id>], the answer the user types, andloginlockdown_captcha_token[<id>], a hidden token set towp_hash(<answer>).
As the form renders, the token is minted straight from the answer:
// libs/functions.php (login_form_fields — token minting, ~line 244)
$output .= '<input ... name="loginlockdown_captcha['.$captcha_id.']" ...>';
$output .= '<input type="hidden" name="loginlockdown_captcha_token['.$captcha_id.']" value="'. wp_hash($captcha['value']) .'" />';Verification recomputes wp_hash() over the submitted answer and compares the result against the submitted token:
// libs/functions.php (handle_captcha — verification, ~line 180)
foreach ($captcha_responses as $captcha_id => $captcha_val) {
if (wp_hash($captcha_val) === $captcha_tokens[$captcha_id]) { // BOTH operands come from $_POST
return true;
} else {
return new WP_Error('lockdown_builtin_captcha_failed', ...);
}
}Both operands of that comparison come from the request. The check only proves that the submitter knows a value and its wp_hash(), which the server itself handed out a moment earlier. It says nothing about this particular interaction. There is no session binding, no nonce, no expiry, no single-use marker. A CAPTCHA is supposed to guarantee that each challenge is solved once and now; here the pair (answer, wp_hash(answer)) is simply a reusable credential.
The missing state produces two results on its own.
- Replay. One valid
(answer, token)pair, captured from a single legitimate form load, satisfies verification on every later submission, indefinitely. - Full-table harvest. The answer space is tiny (sums within 0–20), so a handful of form loads yields a complete lookup table of every
(answer, token)pair. After that, no challenge ever has to be solved again.
4. How the flaw is reached (conceptual)
Reaching the sink takes no authentication. The login form is public, and verification runs on the public login POST. Any unauthenticated visitor can request the login page like a normal browser and read a matched (answer, token) pair straight out of the returned HTML. The answer is the visible math challenge; the token sits next to it as a hidden field. Since the token is nothing more than wp_hash(answer) and the server keeps no record of which pairs have been spent, the verifier cannot tell a fresh human submission apart from a stored pair that a script replays.
The gate stops being a gate. It is a lock whose key is printed on the door. Once any valid pair is known, and the pair is visible to anyone who loads the page, with only a handful of distinct values across the whole answer space, the CAPTCHA step collapses into a constant that an automated client attaches to every request. The anti-automation control becomes a formality, and scripted login attempts against wp-login.php run as though no CAPTCHA were configured at all. We leave out the literal request sequence and values on purpose. The mechanism above is what defenders need to size up the risk.
5. Impact assessment
In practice the login form loses its anti-automation guarantee. With the built-in CAPTCHA selected, an administrator expects automated login attempts to be throttled by a human-verification step. One harvested pair, or the small complete table, lets an automated client clear that step across unlimited attempts. Brute-force and credential-stuffing against the WordPress and WooCommerce login are back on the table, which are exactly the attacks the plugin was installed to resist.
The honest rating is Medium. The vector AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N (~5.3) describes a network-reachable, low-complexity, unauthenticated bypass with no direct confidentiality or availability loss; the score reflects the integrity impact of defeating the control, not a guaranteed account compromise. The precondition tempers it further, because the built-in CAPTCHA is off by default and the problem only applies where an admin turned it on explicitly. The bypass grants no authentication on its own. It removes a layer that stands in front of authentication.
For context, a separate review confirmed the IP-lockout logic is unaffected. It derives the client address from REMOTE_ADDR only and does not trust X-Forwarded-For/Client-IP, so spoofed-IP lockout evasion does not apply, and its database access uses parameterised queries with whitelisted columns, so there is no SQL injection.
6. Remediation
Two changes fix this: stop trusting the client for the expected answer, and make every challenge single-use. The answer belongs on the server, generated and stored there, either in the session or in a short-lived transient keyed by a server-generated id, and deleted the moment verification runs. Shipping wp_hash(answer) to the browser and recomputing a comparison over two client-supplied values has to go. The challenge should also be bound to the session and given an expiry.
// Illustrative hardened flow — server keeps the answer; the client never sees it.
// When rendering the form:
$captcha_id = wp_generate_uuid4();
$answer = /* compute the math answer server-side */;
set_transient( 'lld_captcha_' . $captcha_id, (string) $answer, 5 * MINUTE_IN_SECONDS );
// emit only the challenge text and $captcha_id — never the answer or its hash.
// On verification:
$expected = get_transient( 'lld_captcha_' . $captcha_id );
delete_transient( 'lld_captcha_' . $captcha_id ); // single-use: consume regardless of outcome
if ( $expected !== false && hash_equals( $expected, (string) $submitted_answer ) ) {
return true;
}
return new WP_Error( 'lockdown_builtin_captcha_failed', /* ... */ );Until a fixed release lands, administrators are better served by a well-maintained third-party CAPTCHA or anti-automation option, and should not lean on the built-in math CAPTCHA as their only brute-force defence.
7. Detection and hardening for site owners
- Treat the built-in math CAPTCHA as non-protective for now. If you depend on anti-automation, keep IP lockout enabled (it is sound) and pair it with a maintained, server-validated CAPTCHA or an external challenge rather than the built-in option.
- Keep the plugin updated. Apply any future Login LockDown release that addresses this, and watch the vendor's changelog.
- Enforce least privilege and strong authentication. The real exposure is renewed brute-force and credential-stuffing, so strong unique passwords and two-factor authentication on privileged accounts blunt the downstream damage even when the CAPTCHA is bypassed.
- Watch logs for automation. A burst of login POSTs that carry the same CAPTCHA answer/token field values across many attempts, especially from one or a few source addresses or with little variation in the challenge id, is the signature of a replayed pair. Repeated failed logins at machine speed against
wp-login.phpwarrant the same attention whether or not a CAPTCHA is configured. - Rate-limit and virtual-patch at the edge. A WAF or reverse-proxy rule that rate-limits
wp-login.phpand flags repeated identical CAPTCHA token submissions works as a temporary virtual patch. Scope the rule to the login endpoint so legitimate traffic is left alone.
(These indicators come from the mechanics of the flaw and assume no particular logging product or feature.)
8. Broader lesson
The principle is simple and broken often: a verifier must not trust the client to carry the secret it is meant to check. Whenever the thing being validated, whether a CAPTCHA answer, a one-time code, a price, or a role, is sent to the browser and then sent back for comparison, the control is decorative. Authoritative security state belongs on the server, bound to the specific session and interaction, given an expiry, and consumed once so it cannot be replayed. A hash of a client value is not state. It is the same client value wearing a different coat. Challenge–response controls earn their security from freshness and single use, never from how hard a hash is to reverse.
9. Responsible disclosure
This research follows coordinated disclosure. The vendor has been notified privately, and the full operational detail — the exact reproduction sequence and payload values — is withheld from this public article until a fix is available or the coordinated deadline is reached. Nothing here provides a runnable attack.
All testing was done by Noroxi LLC in an isolated, localhost-bound laboratory against an installation we control. No third-party or production systems were touched, and the plugin was removed once testing finished. Noroxi LLC conducts authorized security research only. We do not test systems without permission, and we do not publish weaponised detail against unpatched software.
References
- CWE-294: Authentication Bypass by Capture-replay — https://cwe.mitre.org/data/definitions/294.html
- CWE-804: Guessable CAPTCHA — https://cwe.mitre.org/data/definitions/804.html
- CWE-287: Improper Authentication — https://cwe.mitre.org/data/definitions/287.html
- WordPress Developer Handbook — Transients API — https://developer.wordpress.org/apis/transients/
- WordPress Developer Handbook — Nonces — https://developer.wordpress.org/apis/security/nonces/
- OWASP — Credential Stuffing — https://owasp.org/www-community/attacks/Credential_stuffing
- FIRST — CVSS v3.1 Calculator — https://www.first.org/cvss/calculator/3.1
Published for defensive and educational purposes. Noroxi LLC conducts authorized security research only; operational exploit detail is withheld until a fix is available.
Exploited this week · every Monday
New KEV entries, fresh PoCs, WordPress triage and what to do this week. No account needed.