EXERCISE 3 — How blind SQLi extracts one character; slow but effective ======================================================================= THE SETUP: no visible query output, no error messages. You can still inject, but you can't SEE the answer. So you ask the database YES/NO questions and read the answer from the app's BEHAVIOUR. Goal: extract, say, the first character of the admin password. BOOLEAN-BASED — read the answer from a PAGE DIFFERENCE: - First, find a reliable "true vs false" tell. Inject: ' AND 1=1-- -> condition TRUE -> page loads normally / shows result ' AND 1=2-- -> condition FALSE -> page differs / "no results" The difference (content, length, status) is your 1-bit readout. - Now replace the constant with a question about the data, using a binary search on the character's ASCII value: ' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)) > 109 -- -> TRUE-page => the 1st char's code is > 109 ' AND ASCII(SUBSTRING(...,1,1)) > 115 -- -> FALSE-page => <= 115 ... keep halving the range (binary search) ... In ~7 requests you pin the exact ASCII value of character 1 (8-bit range). Then move to position 2 (SUBSTRING(...,2,1)), and so on. You can also test equality directly: ' AND SUBSTRING(pw,1,1)='a'-- (TRUE page => it's 'a'). - You've extracted a character purely from whether the page rendered the "true" or "false" variant. TIME-BASED — read the answer from a DELAY (when the page is identical): - If TRUE and FALSE produce the SAME page (no visible difference at all), create your own signal: make the DB PAUSE on "true." ' AND IF(ASCII(SUBSTRING((SELECT password...),1,1)) > 109, SLEEP(5), 0)-- -> if the response takes ~5s longer, the condition was TRUE; if it returns immediately, FALSE. (Postgres: ...AND (CASE WHEN condition THEN pg_sleep(5) ELSE ... END); MSSQL: ...; IF(condition) WAITFOR DELAY '0:0:5'.) - Same binary search as boolean, but the 1-bit answer comes from RESPONSE TIME instead of page content. Pin each character's ASCII value via timed yes/no questions, position by position. WHY BLIND IS SLOWER THAN UNION-BASED BUT EQUALLY EFFECTIVE: - UNION-based returns whole VALUES (even whole tables) in a single response — you read the data directly. Very fast. - BLIND returns only ONE BIT PER REQUEST (true/false), so extracting one character takes several requests (≈7 with binary search), and a long secret takes hundreds/thousands of requests; time-based additionally waits out the SLEEP each time. So it's much SLOWER and noisier. - But it is EQUALLY EFFECTIVE at the goal: given enough requests you recover the SAME data, character by character. Nothing about the secret is protected — only the speed of retrieval differs. And it's trivially AUTOMATED (a script or sqlmap drives the thousands of requests), so "slow" rarely means "safe." WHY CHANNEL-CLOSING DEFENCES DON'T STOP THE UNDERLYING INJECTION: - Hiding output/errors or normalizing responses only removes a READOUT channel; it pushes the attacker from union -> error -> boolean -> time -> out-of-band. The QUERY IS STILL INJECTABLE — the input is still parsed as SQL — so some inferential channel almost always remains (even pure timing). - The injection itself (auth bypass, data modification) also still works regardless of whether you can read results. - Only fixing the CAUSE stops it: PARAMETERIZED QUERIES (Chapter 7) mean the input is never parsed as SQL, so there is no true/false to infer and no delay to measure — the blind oracle disappears along with every other type. ONE-LINE TAKEAWAY: Blind SQLi turns the app into a yes/no oracle (page change = boolean, delay = time-based) and extracts data one character at a time — slow but complete and automatable; only parameterization removes the oracle.