open atlas
↑ Back to track
Offensive Security RED · 02 · 05

Finding and proving bugs

A vulnerability you can't prove to someone else doesn't exist yet — to the triager it's noise. This lesson is the craft of turning a hunch into a report that survives a stranger's machine — a reproducible PoC, an honest impact statement, and disclosure that stays inside scope.

RED Senior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

On an authorized engagement, a tester finds a genuine broken-access-control bug: a settings endpoint returns another tenant’s config. It reproduces flawlessly on their laptop every single time. They write three lines — “log in, GET the endpoint, observe the other tenant’s data” — and submit. Three weeks later the report is closed: not reproducible. The bug was completely real. What the tester forgot to write down was that their account had a preview feature flag enabled and their session token was minted before a deploy — invisible state that lived only on their machine. The triager followed the three lines on a fresh account and got a clean 403. This is the lesson nobody teaches alongside exploitation: a vulnerability you have found and a vulnerability you can prove to a stranger are two different things, and only the second one gets fixed. The whole back half of offensive work — after the clever bug, before the credit — is the unglamorous craft of building proof that survives the only boundary that matters: someone else’s machine. Finding is half the job. Proving is the half that decides whether the finding counts.

By the end of this lesson you’ll be able to turn a confirmed bug into a report that survives triage: a self-contained reproducible PoC, an honest impact-and-severity statement, and disclosure that stays strictly inside scope.

Authorization first: where proving lives

Everything in this lesson assumes a signed engagement with explicit scope — a bug-bounty program with the target in its asset list, a pentest with a rules-of-engagement document, a deliberately vulnerable lab, or your own application. The act of proving a bug is exactly where ethics gets sharpest, because proof is where the temptation to “just show how bad it really is” lives. The discipline this lesson teaches — minimum-necessary proof, scope-respecting impact, coordinated disclosure — is not bureaucratic politeness; it is the line between a researcher and an intruder. A single cross-account read of a record you planted proves an IDOR; dumping ten thousand real customers’ invoices to “demonstrate severity” proves the same bug while committing a fresh data-breach with your own name on it. There are no working exploits or harvested data here — only the reasoning that lets you prove a finding without becoming the incident.

A bug found is not a bug proven

The mental shift senior researchers make is to stop treating the report as a formality and start treating it as the actual deliverable. The vulnerability in the code is not the product you ship; the proof is. And a proof has a brutal acceptance test that has nothing to do with whether the bug is real: can a stranger, on their own machine, follow your steps and see the same thing? If the answer is no, the bug — however real — is indistinguishable from noise, and a busy triager working a queue will close it.

That reframing turns the rest of the job into three concrete artifacts the report must carry, each with its own failure mode:

  • A reproducible PoC — deterministic, minimal, self-contained steps. Fails when it secretly depends on state only you have.
  • An honest impact statement — what the bug lets an attacker actually do, in business terms, at an accurate severity. Fails when it’s inflated or left empty.
  • Disclosure that respects scope — minimum-necessary proof, no real-victim data, private-first reporting. Fails when “showing impact” crosses into causing it.

Reproducibility: the property that decides everything

A PoC is reproducible when it is deterministic, minimal, and self-contained — three properties that fail in different ways, and the failures are why real bugs get closed.

Deterministic means it produces the same result every run, not “sometimes.” The word sometimes in a report is fatal: it guarantees the triager runs your steps once, hits the path where nothing happens, and closes the ticket. A race condition or a state-dependent bug is still provable — but you must pin down the exact conditions that fire it, so “sometimes” becomes “every time, when X.”

Minimal means you’ve stripped the repro to the single variable that matters. Senior PoC-writing is reduction: keep changing one thing and re-running until the steps are the shortest sequence that still demonstrates the bug. A repro a triager can replay in under a minute gets actioned; a twelve-step ritual with three browser extensions and a custom proxy chain gets deferred to “when someone has time,” which is never.

Self-contained is the one that closes real bugs as not-reproducible — the trap from the hook. Your repro must declare every precondition explicitly, because the steps run in the triager’s world, not yours. Hidden assumed state — a feature flag enabled on your account, a token minted before a deploy, a record that exists only because you created it last week, a specific account tier — is invisible to you precisely because it’s always true on your machine. The cure is to write each precondition down (“requires an account with flag F; victim record N must exist”) or, better, to keep reducing until the repro no longer depends on that state at all.

PropertyWhat breaks itWhat the triager recordsThe discipline
Deterministic”You can see other people’s data sometimesRan once, saw nothing → not reproduciblePin the exact firing conditions: “every time, when X”
Minimal12 steps, custom proxy, three extensionsDeferred to “when there’s time”Reduce to the one changed variable; <1-min replay
Self-containedHidden state: feature flag, pre-deploy tokenClean 403 on a fresh account → not reproducibleState every precondition; reduce away hidden state
Observed vs expectedA lone screenshot of a 200 with no baseline”Could be your own data” → unconfirmedState actual (200, B’s data) vs expected (403)

One more element separates a proof from a screenshot: observed-versus-expected. A bare 200 response proves nothing on its own — it could be your own record, a cache artifact, or a session quirk. The proof is the gap: “with account A’s session I requested account B’s record and got 200 returning B’s data, where a correct system returns 403.” Two accounts you control, one variable changed, the actual result named against the expected one. That structure is what makes a cross-account read undeniable rather than a hunch with a picture.

Why this works

Why is “self-contained” the property that closes real bugs, while determinism and minimality mostly close weak ones? Because hidden state is invisible by construction. A non-deterministic or bloated repro at least looks suspicious to its own author — you can feel the “sometimes.” But assumed environment state feels like the floor of reality: the feature flag has been on your account for two days, so of course the endpoint behaves that way; the record has always been there because you made it. You’re not omitting the precondition out of laziness — you genuinely don’t perceive it as a precondition, because it has been constant in every observation you’ve made. That’s why the discipline can’t be “remember to mention preconditions”; it has to be the structural habit of reproducing your own finding from a clean slate — a fresh account, a new token, nothing pre-provisioned — and watching what breaks. The thing that breaks is the precondition you’d never have written down. Self-containment isn’t a documentation step; it’s an experiment you run against yourself.

Impact: the half of the report that sets priority

A reproducible PoC proves the bug exists; the impact statement decides whether anyone cares — and it’s where two opposite failures live.

Inflation is claiming more than you proved: labeling a read-only SSRF to one benign internal status page as “Critical RCE, 10.0.” The mechanism may be real, but if nothing in your report shows code execution or metadata access, the severity is fabricated. The cost isn’t just that the triager downgrades this one report — it’s that they stop trusting every number you submit afterward. Credibility is the currency of disclosure, and you spend it permanently the first time you over-claim. The honest move is to state the proven ceiling and flag the realistic chain separately: “confirmed read-only SSRF to internal hosts; I did not reach the cloud metadata endpoint, but the same primitive plausibly could, which would yield credentials — flagged as the realistic chained risk, not demonstrated.”

Emptiness is the opposite: “could be bad.” A triager can’t prioritize a shrug. A strong impact statement translates the technical primitive into business terms — who can do what to whose data, by what action, and the realistic ceiling. For an IDOR on sequential invoice ids with no rate limit: “any authenticated user can read any other customer’s invoice — name, billing address, card last-four — by requesting an id they don’t own; ids are sequential and unthrottled, so the whole corpus is enumerable, which I describe but do not perform.” That sets a defensible severity (high confidentiality impact, broken access control) and points at the fix.

Severity language is a shared contract — CVSS exists so “high” means roughly the same thing to you and the triager. You don’t need to compute a perfect score, but anchoring to the vector (attack complexity, privileges required, confidentiality/integrity/availability impact) keeps you honest: a read-only cross-tenant leak and a server takeover are not the same number, and pretending otherwise is the inflation failure wearing a formula.

Pick the best fit

On an authorized bug-bounty target with testing-only accounts allowed and no access to real customer data, you confirm an IDOR: with account A's session, GET /api/invoices/{id} returns account B's invoice when B's id is requested. Rules of engagement forbid mass automated requests. You want the proof that gets this fixed fastest without crossing scope. What do you submit?

Disclosure: proving without becoming the incident

The last artifact is conduct, and it has two rules that the temptation to impress will push against.

Minimum-necessary proof. Prove the class with the smallest possible demonstration and stop. One canary record, not the whole table. A single forged token on a test account, not every account. The instinct to “show how bad it really is” by going further is precisely the instinct that turns a clean finding into a crime — because the extra demonstration accesses real data you have no authorization to touch. The class was proven at the first read; everything after is harm without evidential gain.

Coordinated, private-first disclosure. Responsible disclosure means reporting privately to the owner first and granting a reasonable remediation window — commonly 90 days, the de-facto industry default — before any public detail. Publishing a working exploit, or worse a zip of real customer data, before the owner has had a chance to fix weaponizes the bug: it exposes the very users you were supposed to protect and crosses the line from researcher to attacker. A published vuln-disclosure policy or bug-bounty channel formalizes this contract from the other side, giving you a defined intake and SLA and the owner a defined response window.

The asymmetry is the whole argument: a few minutes of explicit preconditions and an honest impact statement on intake, versus a real bug that festers, gets re-discovered, and lands as a public 0-day with an incident attached. The craft of proving is what keeps the finding — and your reputation — on the right side of that line.

Quiz

You report a genuine broken-access-control bug with three terse steps, and it's closed as 'not reproducible.' Your repro works perfectly on your laptop every time. What is the single most likely cause, and the right fix?

Quiz

On an authorized engagement you've confirmed an IDOR on sequential invoice ids with no rate limit. How do you prove the scale of the impact without crossing scope?

Order the steps

Order how a senior researcher turns a confirmed in-scope IDOR into a report that gets fixed:

  1. 1 Reduce to a minimal deterministic repro across two accounts you control, reading one planted-victim record (observed 200 with B's data vs expected 403)
  2. 2 Re-run from a clean slate — fresh account, new token — to surface any hidden preconditions and make the PoC self-contained
  3. 3 Write the honest impact-and-severity statement: who reads whose data, the described enumeration ceiling, an accurate CVSS class, a remediation direction
  4. 4 Disclose privately to the owner with minimum-necessary proof and hold public detail until the remediation window closes
Recall before you leave
  1. 01
    What three properties make a PoC reproducible, how does each fail, and why is self-containment the one that closes genuinely real bugs?
  2. 02
    What are the two failure modes of an impact statement, and what are the two disclosure rules that keep proving a bug from becoming committing one?
Recap

Finding a vulnerability is the entry point, not the deliverable — the report is. A bug you can demonstrate on your own machine and a bug you can prove to a stranger are two different things, and a busy triager fixes only the second, so the back half of offensive work is the craft of building proof that survives someone else’s machine. That proof carries three artifacts. A reproducible PoC must be deterministic (no “sometimes” — pin the firing conditions), minimal (reduced to the one changed variable, replayable in under a minute), and self-contained (every precondition stated, because hidden state — a feature flag, a pre-deploy token, a record only you created — is invisible to you precisely because it’s always true on your box, and it is the thing that closes genuinely real bugs as “not reproducible”); the structural cure is to re-run from a clean slate and show observed-versus-expected across two accounts you control. An honest impact statement translates the primitive into business risk at an accurate CVSS severity, avoiding both inflation (which permanently spends your credibility) and emptiness (which can’t be prioritized), and communicates scale by describing the enumeration ceiling rather than reaching it. Scope-respecting disclosure proves the class with minimum-necessary proof — one planted canary, never real-victim data — and reports privately first with a reasonable remediation window before any public detail. So the next time you confirm a bug, the question that decides whether it counts isn’t “is this real?” — you already know it is — it’s “could a stranger, on their own machine, see exactly what I saw, and have I proven the impact without becoming it?”

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.