bb-sqli
Hunt SQL injection on authorized targets — boolean, time-based, error-based, and UNION SQLi in APIs, filters, sort params, array parameters, legacy PHP/report endpoints, and JSON/GQL inputs, plus safe
SKILL.mdskills/bb-sqli/SKILL.md
GitHubSQLi Hunter
Modern ORMs killed the easy SQLi; survivors hide in ORDER BY columns, search filters, IN-lists, array params, and raw query escape hatches. On production: proof must be non-destructive (boolean diff or sleep ≤3s, once), never sqlmap --batch against the live target without explicit program permission — most programs allow it only throttled, some ban it outright.
Real-bug anchors (what actually paid — query bb-disclosures for more)
- $25,000 SQLi in
report_xml.phpthrough thecountryFilter[]array parameter — legacy report/XML endpoints with array-style params are the top survivor. - $4,500 plain SQLi on a marketing site (
www.drivegrab.com) — forgotten microsites run old code. - $4,263 CVE-2024-42005 Django ORM bypass in
QuerySet.values()— even framework "safe" paths leak. - $4,134 Blind SQLi on
id.indrive.com— blind confirmed by timing/oracle is full-price. Delta lesson: array/suffixed params (param[]), legacy.phpreport endpoints, and forgotten microsites carry the class. Blind is not cheaper.
Where SQLi survives (target list)
- Array/suffixed parameters:
countryFilter[],ids[],filter[status]— implode-into-IN() builders. - Sort/order params:
?sort=name→ORDER BY name— can't be parameterized → classic survivor. Test column-name injection. - Filter builders:
?filter[status]=x&filter[created]=yframeworks building raw WHERE. - Legacy PHP/report/XML endpoints (
report_xml.php-style, export endpoints, cron-facing pages) — wayback CDX finds these forgotten paths. - Search:
?q=, autocomplete, faceted search backends (sometimes NoSQL/Lucene instead — fingerprint first). - IN-lists / batch params:
?ids=1,2,3string-concatenated. - Legacy/second-order: values stored then used in a later raw query (import file names, report generators).
- JSON/GraphQL inputs: string fields that reach raw SQL in resolvers (
filter,searchargs).
Wordlist (bundled)
assets/sql-wordlist.txt — 1,080-entry SQLi payload list (orwagodfather/SQL-Wordlist), URL-encoded, spanning boolean/error/time-based/UNION across MySQL/Postgres/MSSQL/Oracle. Grep by technique instead of replaying the whole file:
grep -i 'sleep' <path-to-skill>/assets/sql-wordlist.txt # time-based (PG_SLEEP, waitfor delay, sleep())
grep -i 'union' <path-to-skill>/assets/sql-wordlist.txt # UNION extraction
grep -iE "or.2\+|=0\+" <path-to-skill>/assets/sql-wordlist.txt # boolean arithmetic
Standout entry that beat strict filters in a paid hunt (Bugcrowd, $20.3k campaign — worked where classic AND SLEEP() was blocked): 1'XOR(SELECT CASE WHEN(1234=1234) THEN SLEEP(7) ELSE 0 END)XOR'Z — XOR/CASE wraps the sleep in arithmetic context, no AND/OR keywords.
Where to aim first (targeting heuristics from paid campaigns)
- Legacy fields populated FROM the database — country/state select dropdowns, sort labels: old columns, old code, often unprotected. Time-based first, value differs from modern API params.
- Forgotten microsites and origin IPs behind CDNs (ASN → CIDR recon) run old PHP where classic
' OR 1=1still lands.
Detection ladder (cheap → expensive, per parameter)
- Error probing (free):
'")```` — response 500/changed error = candidate. Grep responses for SQL fragments (MySQL/Postgres/MSSQL/Oracle error signatures). - Boolean diff:
AND 1=1vsAND 1=2— response length/content differs deterministically (send each twice to rule out noise). - Time-based (blind confirmation):
AND SLEEP(2)/pg_sleep(2)/WAITFOR DELAY '0:0:2'— one request, measure; compare to a control request. Cap at 2-3s, never stack. - UNION (only after confirming):
ORDER BY n--column count, thenUNION SELECT NULL,NULL,...— extraction stops at proving read access (version(),current_user). Do NOT dump user tables on production — oneversion()is the full PoC; anything more risks program violations.
Context escaping cheat sheet
- In string:
' AND '1'='1/ close-quote strategies. In numeric:AND 1=1--. In JSON string value: same payloads, watch double-encoding. In ORDER BY:name)--/(SELECT 1)boolean via sort error. In array params:countryFilter[]=x' AND '1'='1. - WAF-filtered: case (
SeLeCt), comments (SE/**/LECT), URL/double-encoding, unicode homoglyphs — one variation at a time, logged (bb-chains bypass discipline).
NoSQL & friends (fingerprint before assuming SQL)
Mongo: {"$gt":""} / $ne in JSON params. Elasticsearch/Lucene: *-wildcard injection in search. GraphQL: filter-arg injection. Fingerprint via error strings before choosing payloads.
Verdict + evidence
- Confirmed = boolean diff (two clean pairs of requests) OR one time-based hit with control OR
version()output. All three: request + response saved verbatim. - "500 on quote" alone = weak; must reach a deterministic oracle before reporting.
- Impact statement: read access (what tables/schemas visible via
version()+ information_schema count if allowed) — but do not enumerate real customer rows. State capability, not loot. - Never:
DROP,UPDATE,INSERT,LOAD_FILE,xp_cmdshell, mass extraction. On sensitive programs, ask permission before even time-based tests if rules are strict.
Tooling note
sqlmap only: (a) with explicit program allowance, (b) throttled (--delay=1 --threads=1), (c) targeted at ONE confirmed parameter with --technique=B (boolean) preferred over time-based, never --risk=3 --level=5 blind on prod.