$ academy/overview
4 phases 24 modules
bug bounty study program

Not a course.
A curriculum.

Progressive study from core web fundamentals to specialized bug hunting. Each module builds on the last. No gaps, no shortcuts.

your starting point
assumed knowledge — what you already have
Offensive mindset
Burp Suite / Caido
Basic recon
Some APIs / auth / XSS
Good tampering intuition
Basic–intermediate practice
the four phases
Phase 1 Core Web — The foundation everyone assumes they have
HTTP internals, browser security model, SOP, CORS, CSRF, cookies, sessions, storage and cache. The layer where most hunters have gaps.
1.1 HTTP1.2 Headers1.3 Cookies1.4 Sessions1.5 Browser1.6 SOP1.7 CORS1.8 CSRF1.9 Storage1.10 Cache
Phase 2 Auth & APIs — Where the money is
JWT, OAuth, REST, GraphQL, multi-tenant architecture and authorization logic. The classes that consistently produce high-severity findings.
2.1 JWT2.2 OAuth2.3 REST2.4 GraphQL2.5 Multi-tenant2.6 Auth logic
Phase 3 Vulnerabilities — Real offensive hunting
BAC, IDOR, workflow abuse, XSS, DOM XSS, client-side bugs, postMessage and CSP. Modern applications, not textbook examples.
3.1 BAC3.2 IDOR3.3 Workflow3.4 XSS3.5 DOM XSS3.6 Client-side3.7 postMessage3.8 CSP
Phase 4 Recon & Automation — Scale like a hunter
Modern recon, JS review, endpoint discovery, automation from zero, pipelines and AI-assisted workflows.
4.1 Recon4.2 JS review4.3 Discovery4.4 Automation4.5 Pipelines4.6 AI
back to overview
1.1 · Phase 1 — Core Web
HTTP Deep Dive
Phase 1 HTTP protocol request lifecycle
HTTP is the language bugs speak. An IDOR is an HTTP object with broken access control. A CSRF is an HTTP request that shouldn't be possible. If you don't have the right mental model for how HTTP works at a low level, you're hunting with half your vision.
core concepts
How HTTP works internally

HTTP is a stateless request-response protocol over TCP. Every request has an exact structure: METHOD /path HTTP/1.1\r\n, then headers, then \r\n\r\n (the critical separator), then optional body. HTTP/2 uses binary frames but Burp translates it — your workflow doesn't change.

HTTP methods that matter for hunting

GETNo body. Idempotent. But many APIs accept params in body anyway.


PUTReplaces the full resource. Very interesting for IDOR.


PATCHPartial modification. Even more interesting for IDOR.


DELETEAlways test if it works without auth.


OPTIONSCORS negotiation. Reveals what methods the server allows.


Hunter thought: Try PUT /api/users/123 instead of GET. Many backends implement the happy path (GET, POST) but forget to protect less common verbs.

Status codes that matter

401Unauthenticated. No token.
403Authenticated but no permission. THE difference from 401.
404Or is it a 403 disguised? Many backends do this.
500Note what input caused it.


Differential error: If /api/documents/456 returns 403 but /api/documents/9999 returns 404, you confirmed ID 456 exists. That's user enumeration via status code oracle.

Where bugs live in the request lifecycle

Most BAC/IDOR bugs live in the gap between authorization middleware and business logic. The middleware checks 'can you access documents?' but the business logic accepts documentId without verifying that document belongs to you.


Browser → CDN/WAF → LB → Auth MWAuthz MW → Routing → Business logic → DB
The gap between Authz MW and Business logic is where most bugs live.

real http requests
modern SPA request
POST /api/v2/documents/transfer HTTP/1.1 Host: app.target.com Authorization: Bearer eyJhbGciOiJSUzI1NiJ9... Content-Type: application/json Origin: https://app.target.com {"documentId": "doc_abc123", "targetUserId": "usr_xyz789"}
response with information leakage
HTTP/1.1 200 OK Server: nginx/1.18.0 ← leaks stack X-Powered-By: Express ← leaks framework {"success":true,"document":{"id":"doc_abc123","owner":"usr_xyz789", "previousOwner":"usr_original"}} ← unrequested field
what backends get wrong
Trusting X-Forwarded-For and similar headers

Many backends use X-Forwarded-For for rate limiting, geolocation, or IP whitelist bypass. Test:


X-Forwarded-For: 127.0.0.1
X-Real-IP: 127.0.0.1
True-Client-IP: 10.0.0.1

Host header used in URL construction

If the server uses Host to build password reset links or confirmation emails, change it to your domain:


Host: evil.attacker.com

Validating Content-Type but not the body

If the WAF only filters JSON but the backend accepts form data, you can bypass protections. Try switching:


Content-Type: application/x-www-form-urlencoded
Content-Type: multipart/form-data

hunting checklist
hunter mindset
🔍
Every response is evidence
A 403 vs 404 tells you different things. A 500 tells you what input the backend doesn't handle. Read every status code as data.
The UI is not the API
The browser shows what developers want you to see. The API accepts what the backend handles. These are different surfaces.
🎯
Think in layers
A WAF, a proxy, and a backend can interpret the same request differently. That gap is where bugs live.
🔧
Vary one thing at a time
Method, header, content-type, parameter — one variable per request. Otherwise you don't know what caused the behavior.
useful mini script
python http_recon.py
import requests, sys TARGET = sys.argv[1] if len(sys.argv) > 1 else "https://httpbin.org/" LEAK = ["Server","X-Powered-By","X-AspNet-Version","Via","X-Varnish"] SEC = ["Strict-Transport-Security","Content-Security-Policy", "X-Content-Type-Options","X-Frame-Options"] VERBS = ["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"] r = requests.get(TARGET, timeout=10, allow_redirects=False) print(f"Status: {r.status_code}\n") print("[ leakage headers ]") for h in LEAK: if h.lower() in {k.lower() for k in r.headers}: print(f" {h}: {r.headers.get(h)}") print("\n[ missing security headers ]") for h in SEC: if h.lower() not in {k.lower() for k in r.headers}: print(f" MISSING: {h}") print("\n[ verb responses ]") for v in VERBS: try: resp = requests.request(v, TARGET, timeout=5, allow_redirects=False) print(f" {v:10} → {resp.status_code}") except: print(f" {v:10} → ERROR")
assessment — answer to reveal
Q1 /api/documents/456 returns 403. /api/documents/9999 returns 404. What did you learn and how do you use it?
ID 456 exists — the server knows about it and actively denies access. Use this to: (1) enumerate valid IDs by looking for 403s vs 404s, (2) confirm a target resource exists before attempting IDOR, (3) build a map of existing IDs. The backend leaks existence information through error code choice.
Q2 A request includes X-Forwarded-For: 10.0.0.1. What offensive possibilities and what do you test first?
Possibilities: (1) rate limiting bypass — try 127.0.0.1 to appear as localhost, (2) IP whitelist bypass — internal IPs may have elevated privileges, (3) geolocation bypass. Test first: change to 127.0.0.1 and look for any behavioral difference. Even a subtle change in rate limit headers confirms the backend uses this value.
Q3 POST /api/users/update accepts application/json. Give 3 concrete HTTP-related steps to test it.
1 — Verb tampering: try GET, PUT, PATCH, DELETE on the same path. A 405 vs 403 tells you different things. 2 — Content-Type switch: resend as application/x-www-form-urlencoded and multipart/form-data. If accepted, you have a WAF bypass vector. 3 — Response field analysis: every undocumented field in the JSON response (role, is_admin, internal_id) is a mass assignment candidate — send them in the next POST and see if they persist.
back to overview
Module in progress
This module is being written. Start with 1.1 if you haven't yet — each module builds on the last.