October 20, 2026
The Logic Vulnerabilities is Always Worth it
whoami?

By Tomahawk
6 min read
whoami?
hello friends i'm youssef(tomahawk0ctf) a security researcher and bug hunter today we will see 3 Vulnerabilities of different type
-
low hanging fruit with 3 bypasses methods
-
and a Critical bug that leads to ATO
-
medium severity bug
Summry
Two findings from the same program โ a race condition in the password-reset OTP flow leading to full Account Takeover (CVSS 7.1 High), and a broken access control issue in a comment up/downvote system exploitable three different ways. Both were technically solid, soo let's see the first bug
Finding 1: Race Condition in Password Reset OTP Generation โ Account Takeover
The password reset endpoint accepted a simple JSON body:
json
{
"email": "victim@domain.com",
"request_id": "UUID",
"method": "CODE"
}{
"email": "victim@domain.com",
"request_id": "UUID",
"method": "CODE"
}A request_id (UUID) was generated per reset attempt and tied to an OTP delivered to the user's email.
Discovery Process
My first instinct was the obvious one: fire two reset requests at two different emails simultaneously and see what happens. First attempt โ different request_id values were issued for each, and each email got its own distinct OTP. No immediate finding, but a thread worth pulling.
While poking at the "resend code" functionality, I noticed something odd: resending with the same request_id always returned the same OTP โ it wasn't rotating. That told me the OTP was being derived from (or stored against) the request_id, not regenerated fresh per request, and not bound to the email/account itself.
That raised the real question: what happens if two different email addresses end up sharing the same request_id?
Going back to Burp history from the earlier parallel-request test, I confirmed each request normally gets a unique request_id. So I forced the collision manually โ replaying the reset request for two different emails using the same request_id instead of letting the server mint a new one each time.
Result: both emails resolved to the identical OTP.
Root Cause
The OTP was generated and validated against request_id as the primary key, with no server-side binding to the specific email/account that the request was supposed to belong to. Since request_id was attacker-controllable (or at minimum reusable/predictable within a short window), this collapsed into a straightforward race-condition-adjacent IDOR on the OTP itself:
- Attacker initiates a reset for their own account, capturing a valid
request_id+ OTP pair. - Attacker replays a reset request for the victim's email using that same
request_id. - Server issues the same OTP for the victim's account.
- Attacker submits the known OTP against the victim's email โ password reset โ full ATO.
Why It Mattered
Flagged at CVSS 7.1 (High) during triage โ no user interaction required beyond knowing the target email, no rate-limit or binding stopped the collision, and the end state is complete account compromise.
Outcome
Closed as Duplicate. Someone beat me to it. Points awarded, no CVSS-weighted bonus. Standard bug bounty tax.
Finding #2: Broken Access Control in Comment Voting (Up/Downvote)
Context
On the same e-commerce target, product comments supported upvote/downvote, with a "you already voted" guard meant to enforce one vote per user per comment.
I found three independent ways to defeat that guard.
Bypass 1 โ Race Condition
Firing concurrent vote requests (same comment, same session) before the server could commit the "already voted" state let multiple votes land before the check caught up โ classic TOCTOU on the vote-tracking logic.
Bypass 2 โ Session Deletion
Stripping the session value entirely and replaying the vote request still resulted in a successfully processed vote. The endpoint didn't fail closed when the session was absent โ it fell back to an unauthenticated/untracked state and processed the action anyway, with no "already voted" tracking possible since there was nothing to key it against.
Bypass 3 โ Session Tampering
This was the most interesting one. Modifying the session value itself โ specifically truncating/altering the signature portion (session_one.signature โ corrupted) โ and replaying:
- Vote once normally โ blocked correctly on a second attempt ("you already voted").
- Corrupt the session signature โ vote again โ succeeds.
- Corrupt it differently โ vote again โ succeeds again.
This means the backend wasn't validating session integrity before trusting it for authorization/dedup purposes. The "already voted" check was apparently tied to the literal session value rather than a verified, tamper-proof user identity โ so any mutation of the session string that still passed basic parsing was treated as a "new" identity for voting purposes, while presumably still being accepted for the request overall.
Root Cause
Two separate failures stacked together:
- Fail-open on missing/invalid session instead of rejecting the request outright.
- No session signature/integrity verification before using the session as the basis for per-user state (vote dedup).
This is textbook Improper Access Control (CWE-284) / CWE-346 (Origin Validation Error) territory โ not a "logic bug" in the dismissive sense triage gave it. A logic bug is "the business rule is wrong." This is "the business rule exists, and the enforcement mechanism can be trivially bypassed by corrupting attacker-controlled input that should never have been trusted in that state."
Outcome
Report got an "RTFS" response and was closed as RTFS / "logic." The program closed down shortly after. Rough way for both to end, but c'est la vie โ not always your rizq, as you said.
Finding #3: Logic Bug that leads to Account lock
The Setup
Most email-change flows follow the same three-step dance: request โ verify โ commit. You ask to change your email, a confirmation link lands in the new inbox, you click it, and then the change actually takes effect. The assumption baked into that design is that step 3 only happens after step 2 โ ownership has to be proven before anything gets bound to it.
On this target, that assumption turned out to be wrong.
Discovery Process
I went into the email-change feature expecting the usual suspects โ maybe an IDOR on the confirmation token, maybe a way to skip the click entirely. Instead I noticed something almost boring by comparison: the moment you submit a target email and hit "send confirmation link," the platform doesn't wait for anything. It reserves that email address right then, against your account, before the recipient has done a single thing.
To confirm this wasn't just a UI illusion, I checked my own attacker account's email after firing the request โ still unchanged. So the request itself gives the attacker nothing. No new email, no new access. The only thing that changes is on the victim's side: their address is now locked to someone else's pending, unconfirmed request.
That's when it clicked that this wasn't going to be an ATO chain in the obvious sense โ it was a denial-of-access primitive. I don't need the victim to do anything. I don't even need them to know I exist. I just need their email address.
I confirmed the lock was real by trying to use the victim's email myself right after:
- Signup with it โ rejected, "black listed" error.
- Login with it โ fails.
- Password reset with it โ fails.
Then I wanted to know if this was temporary โ surely the pending request expires and releases the email back, right? I let the confirmation token sit unused past its own validity window (roughly 24 hours) and tried again. Same rejection. The lockout outlived the token that created it. No cleanup job, no release, nothing. At that point it stopped being "annoying edge case" and became "indefinite denial of service against an email address you don't own."
Root Cause
The platform conflates two states that should never be conflated: a change was requested and ownership was confirmed. It binds the target email at request-time instead of confirm-time, and has no fallback to release that binding if confirmation never happens. In CWE terms, that's CWE-841 โ Improper Enforcement of Behavioral Workflow: the system has the right steps defined, it just doesn't enforce that they happen in the right order before taking an action with real consequences.
is was out of scope:(
Closing Note
This is the side of bug bounty nobody really talks about โ dupes, "logic" closures, or silence until the program shuts down. It's pretty much all I've seen so far, but I'm sure the accepted day is coming. Thanks for reading.