diff --git a/src/pentesting-web/reset-password.md b/src/pentesting-web/reset-password.md index 92b94bbb8b9..15652f610d3 100644 --- a/src/pentesting-web/reset-password.md +++ b/src/pentesting-web/reset-password.md @@ -16,6 +16,10 @@ - Attackers may manipulate the Host header during password reset requests to point the reset link to a malicious site. - **Impact**: Leads to potential account takeover by leaking reset tokens to attackers. +- **Exploitation tips**: + - Test not only `Host`, but also override headers such as `X-Forwarded-Host`, `Forwarded`, `X-Host`, and `X-Original-Host`. Reverse proxies and middleware sometimes build the reset URL from those values instead of from the canonical host. + - If the reset request contains parameters such as `baseurl`, `return_to`, `redirect_uri`, `redirect_url`, `next`, or a tenant/domain selector, point them to an attacker-controlled host and inspect the email template. + - Try the poisoning payload on the first request **and** on "resend reset link" endpoints. In several real cases only one of the two paths was vulnerable. - **Mitigation Steps**: - Validate the Host header against a whitelist of allowed domains. - Use secure, server-side methods to generate absolute URLs. @@ -80,12 +84,33 @@ email="victim@mail.tld",email="attacker@mail.tld" ```php POST /resetPassword [...] -{"email":["victim@mail.tld","atracker@mail.tld"]} +{"email":["victim@mail.tld","attacker@mail.tld"]} ``` +- Convert a normal form submission to JSON and try nested arrays/objects (some frameworks type-coerce a single-recipient field into multiple recipients) + +```json +{ + "user": { + "email": ["victim@mail.tld", "attacker@mail.tld"] + } +} +``` + +- Try framework-specific multi-value formats + +```http +POST /resetPassword HTTP/1.1 +Content-Type: application/x-www-form-urlencoded + +email[]=victim@mail.tld&email[]=attacker@mail.tld +``` + +- Also try comma-separated values inside a single string (`victim@mail.tld,attacker@mail.tld`) and content-type conversions (`x-www-form-urlencoded` -> JSON, `multipart/form-data` -> JSON) because some mailers or validators split recipients late in the pipeline. - **Mitigation Steps**: - Properly parse and validate email parameters server-side. - - Use prepared statements or parameterized queries to prevent injection attacks. + - Reject arrays / repeated parameters when a single recipient is expected. + - Cast the final recipient to a string before passing it to the mailer and re-validate after any normalization step. - **References**: - [https://medium.com/@0xankush/readme-com-account-takeover-bugbounty-fulldisclosure-a36ddbe915be](https://medium.com/@0xankush/readme-com-account-takeover-bugbounty-fulldisclosure-a36ddbe915be) - [https://ninadmathpati.com/2019/08/17/how-i-was-able-to-earn-1000-with-just-10-minutes-of-bug-bounty/](https://ninadmathpati.com/2019/08/17/how-i-was-able-to-earn-1000-with-just-10-minutes-of-bug-bounty/) @@ -125,10 +150,15 @@ POST /api/changepass - Based on Firstname and Lastname - Based on Date of Birth - Based on Cryptography +- Also request several reset links in parallel and compare them. Identical tokens, deterministic increments, or "one-time" links that remain valid after first use usually indicate a race condition or a state machine bug. - **Mitigation Steps**: - Use strong, cryptographic methods for token generation. - Ensure sufficient randomness and length to prevent predictability. -- **Tools**: Use Burp Sequencer to analyze the randomness of tokens. +- **Tools**: Use Burp Sequencer to analyze the randomness of tokens, and Turbo Intruder / Burp Repeater group-send to test parallel issuance and reuse. + +{{#ref}} +race-condition.md +{{#endref}} ## **Guessable UUID** @@ -155,9 +185,22 @@ uuid-insecurities.md ## **Using Expired Token** - Testing whether expired tokens can still be used for password reset. +- Check both the final reset endpoint and any intermediate "validate token" endpoint. Some applications reject the token on the UI step but still accept it on the JSON/API step that actually changes the password. - **Mitigation Steps**: - Implement strict token expiration policies and validate token expiry server-side. +## **Race / Reuse of One-Time Reset Tokens** + +- Some applications invalidate reset tokens only after the password update finishes, or only in one worker/process. This makes supposedly one-time links reusable when the same token is submitted concurrently. +- Practical checks: + - Open the same reset link in two browsers and submit both almost at the same time. + - Send two `POST /reset` requests in parallel with the same token but different passwords. + - Complete the password change once, then replay the exact final request before following any redirect chain. +- If two different passwords are accepted or the same token works twice, you likely found a race condition in token invalidation. +- **Mitigation Steps**: + - Invalidate the token atomically before or during the password update transaction. + - Ensure the token is single-use across all workers and replicas. + ## **Brute Force Password Reset Token** - Attempting to brute-force the reset token using tools like Burpsuite and IP-Rotator to bypass IP-based rate limits. @@ -168,8 +211,55 @@ uuid-insecurities.md ## **Try Using Your Token** - Testing if an attacker's reset token can be used in conjunction with the victim's email. +- This usually appears when the application validates the token and the target account independently. Typical vulnerable patterns: + - Step 1 (`/forgot-password`) issues a token tied to the attacker account. + - Step 2 (`/reset-password`) accepts both `token` and `email` / `userId` / `username` from the client. + - The backend checks that the token exists, but uses the attacker-controlled identifier to decide **which** password to change. +- Practical mutations to test: + - Keep your valid token but replace `email`, `userId`, `username`, `login`, or `accountId` with the victim's value. + - Move the token between query string, JSON body, and headers while changing the victim identifier in the other location. + - If the reset page first calls a "validate token" endpoint and then a separate "change password" endpoint, change the victim identifier only in the second request. +- **Mitigation Steps**: + - Bind the token to the account on the server side and derive the target user exclusively from that token. + - Do not trust any account identifier submitted alongside the token unless it matches the token owner. + +## **Password Reset Token Disclosure in API Responses** + +- Some APIs return the reset token (`resetToken`, `tempToken`, `recoveryCode`) directly in the forgot-password response or in a secondary polling/debug endpoint. +- This is usually an immediate ATO: trigger a reset for the victim, capture the token from the API response, then call the final reset endpoint without mailbox access. +- Also inspect GraphQL responses, mobile APIs, websocket notifications, batch endpoints, and verbose error messages for leaked token values or token expiry metadata. + +```http +POST /api/v1/account/forgot-password HTTP/1.1 +Content-Type: application/json + +{"email":"victim@example.com"} + +HTTP/1.1 200 OK +Content-Type: application/json + +{"tempToken":"4f89...","tokenExpiry":"","user":{"email":"victim@example.com"}} +``` + +## **Missing "token was generated" check** + +- A subtle variant appears when the reset endpoint compares the attacker-supplied token with the value stored in the database, but never verifies that a reset flow was actually started. +- If the default stored value is `NULL` or an empty string, try sending `null`, `""`, omitting the field entirely, or using the string `"null"`. +- This bug is especially interesting on recently created accounts or on accounts where the application initializes `tokenExpiry` during account creation instead of during the reset flow. + +```json +{ + "user": { + "email": "victim@example.com", + "tempToken": null, + "password": "NewP@ssw0rd!" + } +} +``` + - **Mitigation Steps**: - Ensure that tokens are bound to the user session or other user-specific attributes. + - Reject reset attempts unless the server recorded a live reset request for that account. ## **Session Invalidation in Logout/Password Reset** @@ -177,7 +267,7 @@ uuid-insecurities.md - **Mitigation Steps**: - Implement proper session management, ensuring that all sessions are invalidated upon logout or password reset. -## **Session Invalidation in Logout/Password Reset** +## **Reset Token Expiration** - Reset tokens should have an expiration time after which they become invalid. - **Mitigation Steps**: @@ -307,5 +397,6 @@ Content-Type: application/json - [https://anugrahsr.github.io/posts/10-Password-reset-flaws/#10-try-using-your-token](https://anugrahsr.github.io/posts/10-Password-reset-flaws/#10-try-using-your-token) - [https://blog.sicuranext.com/vtenext-25-02-a-three-way-path-to-rce/](https://blog.sicuranext.com/vtenext-25-02-a-three-way-path-to-rce/) - [How I Found a Critical Password Reset Bug (Registration upsert ATO)](https://s41n1k.medium.com/how-i-found-a-critical-password-reset-bug-in-the-bb-program-and-got-4-000-a22fffe285e1) - +- [GitLab Critical Security Release: 16.7.2, 16.6.4, 16.5.6](https://docs.gitlab.com/releases/patches/patch-release-gitlab-16-7-2-released/) +- [Critical: Unauthenticated Password Reset Token Disclosure Leading to Account Takeover in Flowise Cloud and Local Deployments](https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-wgpv-6j63-x5ph) {{#include ../banners/hacktricks-training.md}}