Thinking like the designer: finding the intended flaw
The hardest CTF challenges have no memory-corruption and no injection - just a system whose logic can be bent. Threat modeling from the attacker's chair: trust boundaries, assumptions, and the questions that find a logic bug.
Some challenges have no exploit in the usual sense. No overflow, no injection, no missing auth check you can point at. The code is clean. And yet there is a way to get the flag, because the system was designed to do something and you can make it do something adjacent that the designer did not intend. These are logic bugs, and they are the hardest and most satisfying category precisely because there is no signature to grep for - you have to understand the design well enough to find where its assumptions break.
The counterintuitive tool for this is a defensive one. Threat modeling - the discipline security engineers use to find flaws in their own designs before shipping - is exactly the lens that finds logic bugs, because a logic bug is just a threat the designer failed to model. Reading a system the way its author should have is how you find what they missed.
Draw the trust boundaries
browser
entirely under the attacker's control
API
the boundary - everything is checked here or nowhere
service
assumes the API validated it
database
assumes everything upstream was honest
Every system has lines across which trust changes: between the browser and the server, between one service and another, between user input and a query, between an unauthenticated and an authenticated request. A trust boundary is where data crosses from something the system does not control into something it does. Bugs live on these lines, because they are where the designer had to decide what to verify - and verification is what gets forgotten.
Sketch the system as boxes (components) and arrows (data flowing between them), and mark every arrow that crosses a boundary. For each crossing, ask the load-bearing question: what does the receiving side assume about this data, and can I violate that assumption? A price sent from the client and trusted by the server, a user id in a token that is decoded but not re-checked, a filename that is validated on upload but not on retrieval - each is a boundary where an assumption is doing work it should not.
Interrogate the assumptions
A logic bug is a broken assumption, so the technique is to enumerate the assumptions and test each one. A structured way to generate them is to walk a threat taxonomy across each component. STRIDE is the classic: for each part of the system, ask whether you can Spoof identity, Tamper with data, Repudiate an action, disclose Information, deny Service, or Elevate privilege. Most CTF logic bugs are the S, T, and E of that list.
| Assumption the design makes | The question that breaks it |
|---|---|
| This value arrives in a valid range | What if it is negative? Zero? Enormous? (A negative quantity refunds money.) |
| This sequence of steps happens in order | What if I do step 3 before step 2, or step 2 twice? (Race conditions, state-machine skips.) |
| This id belongs to the requester | What if I use someone else's id? (Access control.) |
| This operation happens once | What if I fire it a hundred times in parallel? (Double-spend, TOCTOU.) |
| This input is one of the values my UI offers | What if I send a value the UI never shows? (Unlisted enum, admin state.) |
| These two checks see the same data | What if the data changes between them? (Time-of-check to time-of-use.) |
The right-hand column is a reusable attack generator. Point it at each trust-boundary crossing you drew, and the questions produce candidate exploits mechanically - which is how you find logic bugs systematically instead of hoping for inspiration.
The classes that recur
- Negative and boundary numbers. Quantities, balances, and indices that were assumed non-negative. Buying -1 of an item, transferring a negative amount, an integer that wraps.
- Order-of-operations and state machines. Reaching a checkout without paying, resetting a password you skipped a verification step for, replaying a one-time action.
- Race conditions. Two requests that each pass a check because neither has committed yet - redeeming one coupon twice, withdrawing the same balance in parallel.
- Confused identity. The system verifies you are someone but not that you are the right someone - the account-takeover flavour of access control.
- Trusting derived data. A total the client computed, a signature over fields the server does not re-check, a redirect target that was validated in the wrong place.
- Instructions arriving as data. Anything that reads attacker-controlled text and then acts on it, which is exactly the boundary prompt injection crosses - and, where the state is money, the boundary smart contract challenges are built around.
The method
Map the system
Components as boxes, data flows as arrows. On paper, badly, in two minutes.
Mark the crossings
Every arrow that crosses a trust boundary - especially anything coming from the client.
State the assumption
For each crossing: what does the receiver believe about this data?
Walk STRIDE
Mostly spoof, tamper, elevate - against each assumption in turn.
Test the load-bearing one first
The candidate that breaks the most of the design if it is false.
Keep the list
The bug is the assumption you have not falsified yet.