SQL injections in CTF: from detection to operation of UNION-based and blind injections

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
356
Reaction score
584
Deposit
0$
Task on CTF: login form, no prompts. administrator'-- in the username field – and you’re inside in 30 seconds. Next task: the same parameter, but the application does not give out errors or data - only "Welcome back" or an empty page. The first is decided by most participants, the second by units. And it is not in the knowledge of SQL, but in the absence of a systematic approach to the operation of SQL injections in the CTF. A person with five payloads and a clear algorithm for choosing a technique bypasses an “experienced” colleague with cheat sheet for 50 pages – because the second cannot determine the moment when it is time to quit UNION and switch to blind.





According to the OWASP classification – A03:2021 (Injection), according to MITRE ATT&CK – Exploit Public-Facing Application (T1190, Initial Access). According to Verizon DBIR 2025, 26% of all confirmed security breaches are web attacks, and injections remain one of the main vectors. In CTF, the target is simpler – the flag instead of credentials, but the chain of action is identical to the penetration of real testing web applications. Let's look at the full methodology: from the first quotation mark to the extraction of the flag through UNION-based, error-based and blindL injection, with working payloades for MySQL, PostgreSQL and SQLite.






Search for SQL injections in CTF: the algorithm of the first five minutes


[Applicable: any CTF web tasks with forms, URL parameters, cookies, HTTP headers] Read more in our web application pentest review.





Before you start sqlmap, you need to find a sqli vulnerability and determine its type. The algorithm begins with one quotation mark and three possible outcomes.






Single Quote Point Testing


Inserted ' in each suspicious parameter - GET, POST, cookies, headings (X-Forwarded-For, Referer, User-Agent). Three response scenarios:





Syntax error. The answer contains You have an error in your SQL syntax, Unterminated string literal, PG::SyntaxError or an analogue. This is the error-based SQL injection – the fastest way to the flag. The error itself gives information about the database and the structure of the request.





Change of behavior without error. The page is displayed differently: the block disappears, the contents change, the text “Welcome back” disappears. This is a boolean-based SQL injection. The data is extracted through the difference in responses to TRUE/FALSE conditions.





No visible changes. No mistake, no visual difference. Trying temporary delay: ' AND SLEEP(5)-- for MySQL or '; SELECT pg_sleep(5)-- for PostgreSQL. The answer was five seconds late – there is an injection, but time-based blind SQLi. There is no delay – the parameter is either not vulnerable or the WAF cuts the payload.





Works if: the application constructs SQL through line concatence without shielding (in CTF - the vast majority of tasks). Does not work if: parameterized queries (prepared statements) or ORM with automatic parameterization are used; input goes through strict typing before hitting SQL.






Definition of DBMS by behavior


The syntax of payloads is critically different between databases. It is the difference in syntax – the main reason why “universal” payloads break down on tasks of medium complexity. Quick tests:





%23 works as a comment (# in URL-coding) – MySQL/MariaDB. Confirm through @@version
|| Concatenizing the strings — PostgreSQL or SQLite. In MySQL || – logical OR (if not ANSI mode), so we use for MySQL CONCAT()
' UNION SELECT sqlite_version()-- return the result — SQLite (in CTF is common, especially in Python/Flask tasks)
' AND @@version>0-- works — MySQL or MSSQL


The definition of DBMS at an early stage saves tens of minutes: instead of overkilling all variants of payloads, you work with a specific syntax. I always spend the first minute at competitions on this – then I fight back ten times.

UNION-based SQL injection: extract data from the database


[Applicable: CTF tasks where the SELECT result is displayed on the product directories page, user profiles, search]





UNION-based injection is a working horse CTF task on sql injections. Operator UNION attaches to the original request arbitrary SELECT, and the result appears right on the page. According to Acunetix, UNION-based SQLi allows you to combine the results of several SELECT requests into a single HTTP response.





Works if: the SQL query result is displayed in HTML and the number of columns in UNION SELECT coincides with the original request. It does not work if: the application does not display query data to the page - then UNION is useless and you need to switch to blind-technics. It also doesn’t work if the WAF blocks the keyword UNION (bypass – in the section about WAF).






Determination of column number


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





ORDER BY (binary search) Consistently increase the number: ' ORDER BY 1-- - OK, ' ORDER BY 5-- – error, ' ORDER BY 3-- - OK, ' ORDER BY 4-- - mistake. Total three columns. It is behind the log2(N) queries - at 10 columns, this is four attempts instead of ten.





UNION SELECT NULL. Add NULL one at a time: ' UNION SELECT NULL--, then ' UNION SELECT NULL, NULL--, then ' UNION SELECT NULL, NULL, NULL--. The third option worked – three columns. Slower than ORDER BY, but more reliable when ORDER BY is syntactically impossible in the context of a request (e.g., inside a subquery).





After determining the number of columns, we find which of them are displayed on the page: -1' UNION SELECT 'aaa', 'bbb', 'ccc'--. Negative -1 ensures that the original request returns an empty result and does not overlap the UNION data. We look at where the markers surfaced on the page aaa, bbb, ccc – in these positions we submit sub-queries.






Extract chain: tables → columns → flag


Let's say MySQL, three columns, the second is displayed on the page:





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

FROM information_schema.tables

WHERE table_schema=database()--



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

FROM information_schema.columns

WHERE table_name='flag'--



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





Three requests and a flag. In practice, there are a little more (the table may not be called flag, eh s3cr3t_fl4g or users), but the logic is the same.





For SQLite – key difference: instead of information_schema used sqlite_master. Request -1' UNION SELECT 1, sql, 3 FROM sqlite_master WHERE type='table'-- will return the full CREATE TABLE with all the column names. Beginners who are accustomed to MySQL, consistently break down in this transition — information_schema in SQLite simply does not exist. I myself on the first CTF lost on this minute twenty until I thought to check the DBMS.





For PostgreSQL – strict typing of columns. If the original query awaits a string, and you set the number - a mismatch of types. Solution: CAST(1 AS text) instead of 1 in positions where the string type is expected. Without CAST PostgreSQL will refuse, and it is worth precious minutes at the competition.





If the application only processes the first line of the result (LIMIT 1 in the original request or the code takes row[0]), GROUP_CONCAT helps to roll all lines into one. In PostgreSQL analog — string_agg(column, ',').






Error-based SQL injection: operation via DBMS errors


[Applicable: CTF tasks with visible database error messages, debug application mode, verbose error pages]





Error-based SQL injection is the fastest way to pull data when DBMS errors are displayed in an HTTP response. One query – and the desired string directly in the error text.





Works if: database errors are visible in the HTTP response (debug mode included, verbose errors, no try/catch in code). Does not work if: the application intercepts all exceptions and returns the generic error page without technical details.






Verbose errors: extractvalue() and CAST()


For MySQL – function extractvalue() generates an error containing the subquery result. Payload extractvalue(rand(),concat(0x3a,(select flag from flag))) displays the flag directly in the error text. The nuance on which many are stuck: extractvalue() returns maximum 32 characters. Flag longer – use SUBSTRING for reading in portions: extractvalue(rand(),concat(0x3a,substring((select flag from flag),33,32))) will get symbols from 33 to 64.





Alternative for MySQL – error through GROUP BY with FLOOR(RAND(0)*2). Payload 0' AND (SELECT 0 FROM (SELECT count(*), CONCAT((SELECT @@version), 0x23, FLOOR(RAND(0)*2)) AS x FROM information_schema.columns GROUP BY x) y)-- generates error Duplicate entry '10.1.36-MariaDB#0' for key 'group_key' – version of the database directly in the message. Dirty but working trick.





For PostgreSQL – technique with CAST(). Trying to bring a string to a whole number causes an error with data: CAST((SELECT password FROM users LIMIT 1) AS int) gives ERROR: invalid input syntax for type integer: "s3cretP@ss". Password is directly in the error message. The reception is especially useful when the limit on the length of the parameter does not allow the use of conditional responses.






Conditional errors: CASE WHEN for intermediate cases


There is an intermediate situation: the application does not show the error text, but behaves differently with and without an error (HTTP 500 instead of 200, or generic error instead of normal). Here the technique of conditional errors works:





xyz' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a – the condition is false, there is no division by zero, the normal answer
xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a – the condition is true, the division by zero → error


Substitute SUBSTRING(password,1,1)>'m' instead of 1=1, extract the data by shame – as in boolean-blind, but the indicator is error/no-error instead of the visual difference. This technique is found in CTF when the taxi-maker disables the output of data and error messages, but forgets to process the division by zero. Classical underwork.





When the technique does NOT work: the application returns exactly the same HTTP response (code 200, the same body) regardless of the error in SQL. Then there is only time-based blind.






Blind SQL injection: boolean-based and time-based techniques


[Applicable: cookie-based injections, login forms without data output, tasks without displaying errors and query results]





Blind SQL injection is the most mutor type of task in the CTF. The application does not return either the result of the request or the error - only indirect signs. As PortSwigger notes, many real vulnerabilities are blind, and UNION techniques are not fully applicable to them. Here begins a real work.






Boolean-based blind: SUBSTRING and binary search


Works if: the application behaves differently with TRUE and FALSE - the presence or absence of text on the page, the difference in the length of the answer, the change of the HTTP code. Does not work if: the answer is absolutely identical to any result of the request.





A classic example from PortSwigger Web Security Academy – cookies TrackingId. Two test requests determine the presence of an injection:





xyz' AND '1'='1 – the page contains “Welcome back” (TRUE)
xyz' AND '1'='2 – “Welcome back” is missing (FALSE)


The difference is there? Great, then - a symbolic extraction through binary search. Payload xyz' AND SUBSTRING((SELECT password FROM users WHERE username='Administrator'),1,1)>'m' – if TRUE, the first password symbol in the n-z range. Next request: >'t' – if FALSE, symbol between n and t. Then >'p' – if TRUE, between q and t. And so to the exact coincidence. Each character is defined for 6-8 queries when binary search by ASCII-band.





To extract a string of 20 characters – 120-160 HTTP requests. With your hands it is painful, but the algorithm is linear and automated for 30 lines in Python with the library requests or one sqlmap command.





A typical trap of task-makers: one of the functions SUBSTRING, MID(), SUBSTR() blocked filter – the rest work. If SUBSTRING not going through – trying alternatives, they do the same.






Time-based blind SQLi: SLEEP() and IF()


Works if: SQL injection is there, but the application returns exactly the same response with any result of the request. No errors, no visual difference, no change in HTTP code. It does not work if: the server processes requests asynchronously and the DBMS delay does not affect the HTTP response time; the network jitter exceeds the SLEEP value; WAF blocks SLEEP/BENCHMARK/WAITFOR.





Principle: we formulate a condition, with TRUE, we keep the DBMS waiting. The difference in response time is the only channel of information. Slowly? Yeah. Yeah. But sometimes it's the only way to the flag.









For MSSQL: '; WAITFOR DELAY '0:0:5'--. For PostgreSQL: '; SELECT pg_sleep(5)--. MySQL also works BENCHMARK(5000000,ENCODE('MSG','by 5 seconds')) – performs the function of five million times, creating a noticeable delay. BENCHMARK useful when SLEEP blocked WAF - filters are less likely to include this function in the blacklist.





On unstable networks with a high jitter (the delay floats for ± 2 seconds) to distinguish SLEEP(5) from the network lag is difficult. Solution: Increase SLEEP to 10-15 seconds. Rough, but it's reliable.






Using sqlmap for SQL injections in CTF


[Applicable: CTF tasks of any type of SQLi when manual operation is too slow]





sqlmap is an open-source tool for automating the detection and operation of SQL injections. In CTF, the main value is the automation of blind injections, which take the clock with your hands.






Requirements for the environment


Python 2.7 or 3.x (sqlmap supports both versions)
Installation: git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git
Burp Suite Community Edition to intercept HTTP requests (recommended)
Network access to the CTF target server



Key scenarios for using sqlmap


From the saved Burp request - the most reliable way. We intercept the vulnerable request in Burp Repeater, save through Copy to file, transfer sqlmap: python sqlmap.py -r request.txt --batch. Flag --batch automatically answers all questions – on the CTF every second counted.





Indicating DBMS and equipment – speeds up the work 3-5 times. If you have already identified MySQL and time-based blind: python sqlmap.py -r request.txt --dbms=mysql --technique=T --batch. Parameter --technique takes the values: B (boolean), E (error), U (union), S (stacked), T (time), Q (inline queries). The indication of a particular technique excludes the overkill of the rest - and this is tangible.





Full chain of extraction built consistently: --dbs for the database list → -D target_db --tables for tables → -D target_db -T flag --columns for columns → -D target_db -T flag -C flag --dump for the data. On the CTF sometimes works --dump-all, but on bases with dozens of tables it is unreasonably slow.





Boolean-blind with TRUE response marker. If sqlmap does not determine the response type automatically, specify the marker: python sqlmap.py -r request.txt --string="Welcome back" --batch. Flag --string informs sqlmap which text in the answer means TRUE.





When sqlmap does NOT help: non-standard injection point (adjusted JSON, GraphQL variables), custom WAF with rate-limiting (sqmap will block by IP after a dozen requests), tasks with two-step logic (the result of the first query determines the structure of the second). In such cases, manual operation or custom Python script. sqlmap is a hammer, but not every task is a nail.






Bypassing WAF at SQL injection in practice


[Applicable: CTF tasks with input filtering, web applications with WAF/regexp filters]





On the CTF of medium and high complexity, the payloads are filtered. Knowledge of WAF bypass techniques is the difference between a solved and unsolved task.





It works if: WAF or server filter blocks specific keywords or characters (SELECT, UNION, spaces, quotes, commas) through regexp or string matching. It doesn't work if: WAF uses SQL semantic analysis (AST query parsit) rather than text matching.






Basic methods of bypassing filters


Change of the register. SQL is a registered language. WAF blocks SELECT and UNION? Trying SeLeCt and uNiOn. According to Invicti, sElEcT will be performed in the same way as SELECT. The simplest bypass, which works on elementary filters with direct comparison of strings. It's funny, but on the CTF, it works more often than would be desirable.





Inline comments instead of spaces. Does the WAF block the gap? Replace on /**/: SELECT/**/flag/**/FROM/**/flag instead of SELECT flag FROM flag. For keyword breakup: UN/**/ION/**/SEL/**/ECT. Inline comments are a universal technique that works in all major databases.





Hex-coding of strings. Quotes are filtered - the string values are transmitted in hex. Instead of WHERE table_name='flag' Write WHERE table_name=0x666C6167. In MySQL 0x666C6167 interpreted as a string flag. For PostgreSQL we use CHR(): CHR(102)||CHR(108)||CHR(97)||CHR(103).





Double URL coding. If the WAF decodes the URL once and the application decodes again, double encoding passes the filter: %2527 → WAF sees %27 and does not block → application decodes in '.





MySQL-specific comment. Construction /*!50000SELECT*/ will only be executed in MySQL version 5.0+ – WAF may not recognize this as a SQL team. Special MySQL syntax for conditional code execution according to the version. It's a tricky thing.





Bypassing comma filtration. Commass is forbidden UNION SELECT with multiple columns is not possible directly. Bypassing through JOIN: UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c. Tested in practice in CTF-writeups – works.





alternative functions. SUBSTRING blocked? Trying MID(), SUBSTR(), LEFT(), RIGHT(). SLEEP blocked? We use BENCHMARK(5000000,SHA1('test')). Filtered AND? Trying &&. Each database has dozens of synonymous functions – the full list is in SQL Injection Cheat Sheet from Invicti.






When the WAF is not working


A new generation of semantic WAF (based on libinjection or analogues) tokenize the SQL request and analyze its structure rather than text patterns. Comments, hex-coding and changing the register do not deceive them. On CTFs, such WAFs are rare, but if you meet, you need to look for a vulnerability in filtering logic: HTTP parameter pollution (duplicating the parameter with different values), the difference in parsing between the WAF and the backend application, or the non-standard Content-Type, which WAF does not inspect.







Most CTF players who get stuck on SQL tasks make one systemic mistake: trying to learn 200 payloads by heart instead of understanding the logic of technique selection. Payload is a consequence. The reason is understanding how specific database processes input and what exactly you observe in the server response. At the competition, you can see how a participant with five payloads and a clear decision tree bypasses a colleague with cheat sheet for 50 pages, because the second cannot determine the moment when it is time to switch from UNION to blind.





The second problem is the overestimated complexity of blind injections. “Pharmacy” sounds intimidating, and in fact – binary search on the ASCII table: 7-8 requests for a symbol, 160 requests for a 20-character password. Automated for 30 lines in Python or one sqlmap command. Another thing is really underestimated – time that is spent trying to make UNION work where it cannot work in principle: the application does not display data on the page, and the participant continues to pick up the number of columns instead of switching to slid.





CTF-tasks for the operation of SQL injections will become more complicated towards non-standard injection points – JSON API, GraphQL variables, WebSocket frames. Classic ' OR 1=1-- in the GET-parameter will remain in tasks for beginners, and at serious competitions the injection will be hidden in the title or in the nested JSON object. The one who has learned to systematically test each point of input closes the tasks until the rest can get out of the first quotation. If you want to bring this methodology to automatism on dozens of labs with a progression from bypassing the login to advanced blind-technician – WAPT closes the web part of the preparation for OSCP with a mentor in the chat.
 
Top Bottom