Search SQL injections manually: from entry point to confirmation
Where to find injection point
A typical mistake is to focus only on GET parameters in the URL. In CTF injection hides anywhere: in cookies, headlines (X-Forwarded-For, Referer, User-Agent), POST data, JSON API fields. In practice sqlmap with default --level=1 does not test cookies, so blind SQLbany times have I seen this on a CTF with atypical injection points is not counted.
The first 60 seconds on any web-task: open the page, Ctrl+U for source code, DevTools for answer titles. Title X-Powered-By often issues a stack (PHP, Flask, Express). If the source code is given, I am immediately looking for the concatenation of variables in SQL requests: "SELECT ... WHERE id = '""'". Concatenation instead of parameterized queries is the root cause of injections into databases, and in CTF the authors love it.
In parallel, I check the hidden endpoints: ffuf -u http://target/FUZZ -w common.txt with verification .git/HEAD, /backup, /admin. Leaking the source code on the CTF is common, and it solves the problem faster than any phase.
Injection confirmation
The first step is to insert a single quotation ' in a suspicious parameter and see what happens:
SQL error in response — error-based, data is extracted fastest
Content is gone or changed – probably boolean-based blind
Nothing has changed, we try ' AND SLEEP(5)-- for time-based
Filtered Quote – bypass techniques (double coding, multi-byte characters) are needed
After confirming the injection type, I determine the number of columns. Method ORDER BY N – increase N from 1 until I get a mistake. If ORDER BY 3 working, huh ORDER BY 4 breaks the request - in the original SELECT three columns. Alternative – UNION SELECT NULL,NULL,... with the selection of the number of NULL (NULL is compatible with most types of data, unlike numerical literals - less chance to run into the type mismatch).
UNION-based SQL injection in practice
UNION-based is the most grateful type for the CTF. The result is visible immediately, no need to wait for delays or to go through the symbols. The algorithm that I have worked out before the automatism:
Step 1: determining the number of columns. ' ORDER BY 1--, ' ORDER BY 2--,... until it breaks. Let's say 3 columns.
Step 2: define the displayed positions. ' UNION SELECT 1,2,3-- I see what number appeared on the page. If “2” is displayed – the second position and there is a “window” for outputting data.
Step 3: Exploration of the database structure. Instead of the number 2, the substitute table_name of information_schema.tables. Construction ' UNION SELECT 1,table_name,3 FROM information_schema.tables-- show the names of the tables. Then column_name of information_schema.columns with filter according to the desired table.
Step 4: extracting data. When I know the table and columns - ' UNION SELECT 1,password,3 FROM users--. The flag usually lies in a separate table with a “speaking” name (flag, secret, s3cr3t_t4bl3).
A typical mistake is to try to read immediately users. In CTF the structure of the base is arbitrary, and without information_schema guessing the blind names. It happens and more tricky: in some CTF machines, standard SQL injection payloads do not work because the identifiers (tables/column names) are interpolated through backtick-quoting, and user data goes through the prepared statements. Here, without analysis of the sources, you can not understand - you need to understand which parts of the request are parameterized and which are companied manually.
A separate story is SQLite. In CTF, this database occurs disproportionately often: Flask + SQLite is a standard stack for simple web-tasks. SQLite does not information_schema - instead of him SELECT name FROM sqlite_master WHERE type='table'. Sqlmap at --dbms=SQLite switches automatically, but during manual operation beginners regularly break at this point. Every second question in the forums is “why information_schema not working?!”
Blind SQLi: boolean-based and time-based approaches
Boolean-based SQLi: when page answers yes/no
The application does not show data from the request, but reacts differently to the true and false conditions. Classic indicator: the word "Welcome" when true and its absence with false.
Manual algorithm: creating a condition ' AND SUBSTRING(password,1,1)='a'-- and I'm going through the symbols. Binary search speeds up the process: instead of overkilling all 95 printed characters, ASCII compares with the middle of the range (' AND ASCII(SUBSTRING(password,1,1))>64--), narrowing the area to the desired symbol for 7 requests.
With your hands, it’s painfully slow – one symbol in 30-40 seconds, a line of 32 characters – almost 20 minutes of clean work. But the first 2-3 characters should be pulled out manually: this confirms the format of the data and the validity of payload before starting the automation. Without this step, then half an hour you will wonder why sqlmap is giving out garbage.
Time-based: the last frontier of operation
When neither the conclusion nor the difference in the answer, there is time. Payload ' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)-- makes the server think for 5 seconds with a guessed symbol. The slowest way: one symbol is 35-50 seconds, taking into account network delays.
In CTF time-based is found in two scenarios: when the author of the task wants to make life difficult (mediam/hard tucky) and when the application uses INSERT/UPDATE Queries – UNION is not possible by definition.
Flag sqlmap --time-sec sets the basic delay (5 seconds by default). On unstable networks with a high jitter increase --time-sec, to reduce the number of false positives. On one CTF with a VPN in half the world I put --time-sec=10 – otherwise sqlmap confused network delays with real SLEEPs.
Error-based SQL injection: fast way to data
Error-based is the middle ground between the UNION speed and the limited slid. The application does not output the result of the request, but shows the DBMS errors. The essence: to slam the target data into the text of the error.
For MySQL classic payload — ' AND ExtractValue(1, CONCAT(0x7e, (SELECT version())))--. The server will return the view error XPATH syntax error: '~5.7.34', where 5.7.34 – version of the database. Similarly works UpdateXML(). For PostgreSQL – CAST() with a deliberately unvalued type.
In CTF error-based sometimes disguises: the application catches exceptions and returns generic "Some where to sell," but with certain types of errors, the text slightly differs. Here helps sqlmap with option --string or --regexp for accurate distinction of answers. The difference may not be noticed with the eyes, and sqlmap with the right marker will notice.
Automation of SQL injections with sqlmap in practice
When to switch to automation
Clear criterion: sqlmap connects after I manually confirmed three things – the presence of an injection, its type, and a specific point in the query. Without this, sqlmap will blindly sort through hundreds of payloads, generate tons of traffic, and probably find nothing – but the IP will be banned.
As the author of SQLMap Cheat Sheet on highon.coffee: “I personally use SQLMap as an tool exploitation, due to the large amount of resources and resources the very top of the work the private I find that detect is better improved manually or using other investigation tools such as Burp Suite scanner.” I fully confirm from my own experience on dozens of CTF tasks.
Integration with Burp Suite
The most reliable way to transfer sqlmap context to a request is to save an HTTP request from Burp Suite to a file and run through -r. This saves all titles, cookies and POST data. If the injection point is non-standard (cookie, title), in the file I mark it with a symbol *:
GET /search?q=test HTTP/1.1
Host: ctf.example.com
Cookie: session=abc123; trackingId=xyz*
Team: sqlmap -r request.txt --dbms=MySQL --technique=B --string="Welcome". I immediately transmit the type of database, technique and line-marker of the true answer. The number of test requests is reduced by an order of magnitude. According to the documentation sqlmap: "the more information you can give SQLMap faster the faster and requests less the tool will make." Logically, the less you guess, the faster the result.
Subtle setup: --level, --risk and --technique
Flags --level (1-5) and --risk (1-3) determine the number and aggressiveness of test payloads. On --level=1 (default) only GET and POST parameters are tested. --level=2 adds the cookie, --level=3 – User-Agent and Referer, --level=5 - all the headlines. For CTF-tasks with injection in cookies, a minimum is required --level=2, otherwise sqlmap simply will not check the desired parameter. I was burned on this more than once.
--risk=1 uses secure payloads. --risk=3 adds OR-based designs and UPDATE-expressions - in a real pentest, this can modify the data, but on the CTF it is permissible.
Flag --technique accepts a combination of letters: B (boolean), E (error), U (union), S (stacked), T (time), Q (inline). The default is all six (BEUSTQ), although the actual set of tested techniques also depends on --level/--risk. If the type of injection is already known, I specify a specific letter: --technique=U for UNION. Removes unnecessary checks and saves time.
--prefix and --suffix for non-standard contexts
When the SQL query on the server is wrapped in a non-standard design, the standard sqlmap payloads do not form a syntactically valid SQL. Flags --prefix and --suffix set the lines before and after each payload.
For requesting a view SELECT ... WHERE name = ' + input + ' prefix – ' (quotation and space), suffix — -- a (comment with the symbol after the space - so that the DBMS correctly recognizes the comment). In CTF, these flags are saved by brackets, double quotes and specific functions of the wrapper. Without them, sqlmap can spend hours chasing unvalid payloads and give out "parameter do not do to be injectable."
Bypass WAF and filters: sqlmap tamper scripts
In the medium and higher categories, the authors almost always add input filtering. The simplest case is the replacement of keywords (SELECT, UNION). More sophisticated – space filtering or multi-byte processing.
Sqlmap comes with a set of tamper scripts through the flag --tamper=script_name. The most useful for CTF:
space2comment – replaces spaces with /**/, bypasses space filters
between – replaces > on NOT BETWEEN 0 AND, bypasses the filters of comparison operators
randomcase – changes the register of keywords (SeLeCt instead of SELECT)
charencode – URL encodes payload
equaltolike – replaces = on LIKE
Scripts are combined: --tamper=space2comment,randomcase,between. But tamper scripts significantly increase the number of requests, and on CTF servers with hard rate limiting it is better to use them on a point-by-point basis - first one, check, then add the second. It is not necessary to drop the whole arsenal at once.
A separate case is bypassing WAF with multi-byte encodings. A classic example (found, in particular, on root-me.org) is a task to bypass the shielding in the GBK encoding, where addslashes() costs through GBK symbols. Mechanism: shielding function finds byte of single quotation (0x27) and inserts backlashes in front of him (0x5c). But if the baxle is facing a leading GBK byte from the range 0x81–0xFE (e.g., 0xbf), then the combination of this byte with 0x5c forms a valid two-byte symbol GBK – backlash is “absorbed” and the quote 0x27 remains unscreened. Payload: admin%bf' OR 1=1 --. A beautiful trick. Protection – mysql_real_escape_string() instead of addslashes() and parameterized requests.
When sqlmap is powerless: custom exploits
Sqlmap covers most standard scenarios, but in the CTF there are regular situations when automation passes: custom input processing logic (encryption of cookies before use in the request), rated limiting on the server, multi-stage payload processing (URL decode → base64 decode → SQL). Here sqlmap stupidly does not know how to prepare payload, and you have to write your script.
For boolean-based blind, the basic logic is to re-select the positions of the symbol with binary search by ASCII code:
import requests
url = "http://ctf.example.com/login"
flag = ""
for pos in range(1, 64):
low, high = 32, 126
while low < high:
mid = (low + high) // 2
payload = f"' AND ASCII(SUBSTR((SELECT flag FROM s3cret),{pos},1))>{mid}-- "
r = requests.post(url, data={"user": payload, "pass": "x"})
if "Welcome" in r.text:
low = mid + 1
else:
high = mid
flag += chr(low)
print(f"[+] {flag}")
This script is a template that adapts to each weight: the URL changes, the sending method, the truth marker, and the SQL design. The core – binary search for ASCII – remains unchanged. On one CTF, where sqlmap failed to cope with the cookie-based injection through non-standard processing, a similar 20-line script pulled the flag in 4 minutes. Sometimes 20 Python lines solve a problem better than thousands of sqlmap strings.
Operation of SQL injections: working checklist for CTF
The order of action that saves time at competitions:
Exploration of the stack — X-Powered-By, source code, comments in HTML. Identify DBMS (MySQL, PostgreSQL, SQLite) before injection
Manual checking is a single quotation rig in each parameter (GET, POST, cookie, headers). Looking for SQL errors, content changes, response delays
Type definition — error-based (error visible), UNION-based (data in response), boolean-blind (different content), time-blind (different time)
Columns – ORDER BY N with increment, then UNION SELECT NULL,... with selection of quantity
Manual extraction of 2-3 characters - confirm the format of the data and the validity of payload
Transition to sqlmap — sqlmap -r request.txt --dbms=X --technique=Y --string="Z", indicating everything I learned manually
Dump – --dbs, then -D db_name --tables, then -D db_name -T table_name --dump
Where to find injection point
A typical mistake is to focus only on GET parameters in the URL. In CTF injection hides anywhere: in cookies, headlines (X-Forwarded-For, Referer, User-Agent), POST data, JSON API fields. In practice sqlmap with default --level=1 does not test cookies, so blind SQLbany times have I seen this on a CTF with atypical injection points is not counted.
The first 60 seconds on any web-task: open the page, Ctrl+U for source code, DevTools for answer titles. Title X-Powered-By often issues a stack (PHP, Flask, Express). If the source code is given, I am immediately looking for the concatenation of variables in SQL requests: "SELECT ... WHERE id = '""'". Concatenation instead of parameterized queries is the root cause of injections into databases, and in CTF the authors love it.
In parallel, I check the hidden endpoints: ffuf -u http://target/FUZZ -w common.txt with verification .git/HEAD, /backup, /admin. Leaking the source code on the CTF is common, and it solves the problem faster than any phase.
Injection confirmation
The first step is to insert a single quotation ' in a suspicious parameter and see what happens:
SQL error in response — error-based, data is extracted fastest
Content is gone or changed – probably boolean-based blind
Nothing has changed, we try ' AND SLEEP(5)-- for time-based
Filtered Quote – bypass techniques (double coding, multi-byte characters) are needed
After confirming the injection type, I determine the number of columns. Method ORDER BY N – increase N from 1 until I get a mistake. If ORDER BY 3 working, huh ORDER BY 4 breaks the request - in the original SELECT three columns. Alternative – UNION SELECT NULL,NULL,... with the selection of the number of NULL (NULL is compatible with most types of data, unlike numerical literals - less chance to run into the type mismatch).
UNION-based SQL injection in practice
UNION-based is the most grateful type for the CTF. The result is visible immediately, no need to wait for delays or to go through the symbols. The algorithm that I have worked out before the automatism:
Step 1: determining the number of columns. ' ORDER BY 1--, ' ORDER BY 2--,... until it breaks. Let's say 3 columns.
Step 2: define the displayed positions. ' UNION SELECT 1,2,3-- I see what number appeared on the page. If “2” is displayed – the second position and there is a “window” for outputting data.
Step 3: Exploration of the database structure. Instead of the number 2, the substitute table_name of information_schema.tables. Construction ' UNION SELECT 1,table_name,3 FROM information_schema.tables-- show the names of the tables. Then column_name of information_schema.columns with filter according to the desired table.
Step 4: extracting data. When I know the table and columns - ' UNION SELECT 1,password,3 FROM users--. The flag usually lies in a separate table with a “speaking” name (flag, secret, s3cr3t_t4bl3).
A typical mistake is to try to read immediately users. In CTF the structure of the base is arbitrary, and without information_schema guessing the blind names. It happens and more tricky: in some CTF machines, standard SQL injection payloads do not work because the identifiers (tables/column names) are interpolated through backtick-quoting, and user data goes through the prepared statements. Here, without analysis of the sources, you can not understand - you need to understand which parts of the request are parameterized and which are companied manually.
A separate story is SQLite. In CTF, this database occurs disproportionately often: Flask + SQLite is a standard stack for simple web-tasks. SQLite does not information_schema - instead of him SELECT name FROM sqlite_master WHERE type='table'. Sqlmap at --dbms=SQLite switches automatically, but during manual operation beginners regularly break at this point. Every second question in the forums is “why information_schema not working?!”
Blind SQLi: boolean-based and time-based approaches
Boolean-based SQLi: when page answers yes/no
The application does not show data from the request, but reacts differently to the true and false conditions. Classic indicator: the word "Welcome" when true and its absence with false.
Manual algorithm: creating a condition ' AND SUBSTRING(password,1,1)='a'-- and I'm going through the symbols. Binary search speeds up the process: instead of overkilling all 95 printed characters, ASCII compares with the middle of the range (' AND ASCII(SUBSTRING(password,1,1))>64--), narrowing the area to the desired symbol for 7 requests.
With your hands, it’s painfully slow – one symbol in 30-40 seconds, a line of 32 characters – almost 20 minutes of clean work. But the first 2-3 characters should be pulled out manually: this confirms the format of the data and the validity of payload before starting the automation. Without this step, then half an hour you will wonder why sqlmap is giving out garbage.
Time-based: the last frontier of operation
When neither the conclusion nor the difference in the answer, there is time. Payload ' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)-- makes the server think for 5 seconds with a guessed symbol. The slowest way: one symbol is 35-50 seconds, taking into account network delays.
In CTF time-based is found in two scenarios: when the author of the task wants to make life difficult (mediam/hard tucky) and when the application uses INSERT/UPDATE Queries – UNION is not possible by definition.
Flag sqlmap --time-sec sets the basic delay (5 seconds by default). On unstable networks with a high jitter increase --time-sec, to reduce the number of false positives. On one CTF with a VPN in half the world I put --time-sec=10 – otherwise sqlmap confused network delays with real SLEEPs.
Error-based SQL injection: fast way to data
Error-based is the middle ground between the UNION speed and the limited slid. The application does not output the result of the request, but shows the DBMS errors. The essence: to slam the target data into the text of the error.
For MySQL classic payload — ' AND ExtractValue(1, CONCAT(0x7e, (SELECT version())))--. The server will return the view error XPATH syntax error: '~5.7.34', where 5.7.34 – version of the database. Similarly works UpdateXML(). For PostgreSQL – CAST() with a deliberately unvalued type.
In CTF error-based sometimes disguises: the application catches exceptions and returns generic "Some where to sell," but with certain types of errors, the text slightly differs. Here helps sqlmap with option --string or --regexp for accurate distinction of answers. The difference may not be noticed with the eyes, and sqlmap with the right marker will notice.
Automation of SQL injections with sqlmap in practice
When to switch to automation
Clear criterion: sqlmap connects after I manually confirmed three things – the presence of an injection, its type, and a specific point in the query. Without this, sqlmap will blindly sort through hundreds of payloads, generate tons of traffic, and probably find nothing – but the IP will be banned.
As the author of SQLMap Cheat Sheet on highon.coffee: “I personally use SQLMap as an tool exploitation, due to the large amount of resources and resources the very top of the work the private I find that detect is better improved manually or using other investigation tools such as Burp Suite scanner.” I fully confirm from my own experience on dozens of CTF tasks.
Integration with Burp Suite
The most reliable way to transfer sqlmap context to a request is to save an HTTP request from Burp Suite to a file and run through -r. This saves all titles, cookies and POST data. If the injection point is non-standard (cookie, title), in the file I mark it with a symbol *:
GET /search?q=test HTTP/1.1
Host: ctf.example.com
Cookie: session=abc123; trackingId=xyz*
Team: sqlmap -r request.txt --dbms=MySQL --technique=B --string="Welcome". I immediately transmit the type of database, technique and line-marker of the true answer. The number of test requests is reduced by an order of magnitude. According to the documentation sqlmap: "the more information you can give SQLMap faster the faster and requests less the tool will make." Logically, the less you guess, the faster the result.
Subtle setup: --level, --risk and --technique
Flags --level (1-5) and --risk (1-3) determine the number and aggressiveness of test payloads. On --level=1 (default) only GET and POST parameters are tested. --level=2 adds the cookie, --level=3 – User-Agent and Referer, --level=5 - all the headlines. For CTF-tasks with injection in cookies, a minimum is required --level=2, otherwise sqlmap simply will not check the desired parameter. I was burned on this more than once.
--risk=1 uses secure payloads. --risk=3 adds OR-based designs and UPDATE-expressions - in a real pentest, this can modify the data, but on the CTF it is permissible.
Flag --technique accepts a combination of letters: B (boolean), E (error), U (union), S (stacked), T (time), Q (inline). The default is all six (BEUSTQ), although the actual set of tested techniques also depends on --level/--risk. If the type of injection is already known, I specify a specific letter: --technique=U for UNION. Removes unnecessary checks and saves time.
--prefix and --suffix for non-standard contexts
When the SQL query on the server is wrapped in a non-standard design, the standard sqlmap payloads do not form a syntactically valid SQL. Flags --prefix and --suffix set the lines before and after each payload.
For requesting a view SELECT ... WHERE name = ' + input + ' prefix – ' (quotation and space), suffix — -- a (comment with the symbol after the space - so that the DBMS correctly recognizes the comment). In CTF, these flags are saved by brackets, double quotes and specific functions of the wrapper. Without them, sqlmap can spend hours chasing unvalid payloads and give out "parameter do not do to be injectable."
Bypass WAF and filters: sqlmap tamper scripts
In the medium and higher categories, the authors almost always add input filtering. The simplest case is the replacement of keywords (SELECT, UNION). More sophisticated – space filtering or multi-byte processing.
Sqlmap comes with a set of tamper scripts through the flag --tamper=script_name. The most useful for CTF:
space2comment – replaces spaces with /**/, bypasses space filters
between – replaces > on NOT BETWEEN 0 AND, bypasses the filters of comparison operators
randomcase – changes the register of keywords (SeLeCt instead of SELECT)
charencode – URL encodes payload
equaltolike – replaces = on LIKE
Scripts are combined: --tamper=space2comment,randomcase,between. But tamper scripts significantly increase the number of requests, and on CTF servers with hard rate limiting it is better to use them on a point-by-point basis - first one, check, then add the second. It is not necessary to drop the whole arsenal at once.
A separate case is bypassing WAF with multi-byte encodings. A classic example (found, in particular, on root-me.org) is a task to bypass the shielding in the GBK encoding, where addslashes() costs through GBK symbols. Mechanism: shielding function finds byte of single quotation (0x27) and inserts backlashes in front of him (0x5c). But if the baxle is facing a leading GBK byte from the range 0x81–0xFE (e.g., 0xbf), then the combination of this byte with 0x5c forms a valid two-byte symbol GBK – backlash is “absorbed” and the quote 0x27 remains unscreened. Payload: admin%bf' OR 1=1 --. A beautiful trick. Protection – mysql_real_escape_string() instead of addslashes() and parameterized requests.
When sqlmap is powerless: custom exploits
Sqlmap covers most standard scenarios, but in the CTF there are regular situations when automation passes: custom input processing logic (encryption of cookies before use in the request), rated limiting on the server, multi-stage payload processing (URL decode → base64 decode → SQL). Here sqlmap stupidly does not know how to prepare payload, and you have to write your script.
For boolean-based blind, the basic logic is to re-select the positions of the symbol with binary search by ASCII code:
import requests
url = "http://ctf.example.com/login"
flag = ""
for pos in range(1, 64):
low, high = 32, 126
while low < high:
mid = (low + high) // 2
payload = f"' AND ASCII(SUBSTR((SELECT flag FROM s3cret),{pos},1))>{mid}-- "
r = requests.post(url, data={"user": payload, "pass": "x"})
if "Welcome" in r.text:
low = mid + 1
else:
high = mid
flag += chr(low)
print(f"[+] {flag}")
This script is a template that adapts to each weight: the URL changes, the sending method, the truth marker, and the SQL design. The core – binary search for ASCII – remains unchanged. On one CTF, where sqlmap failed to cope with the cookie-based injection through non-standard processing, a similar 20-line script pulled the flag in 4 minutes. Sometimes 20 Python lines solve a problem better than thousands of sqlmap strings.
Operation of SQL injections: working checklist for CTF
The order of action that saves time at competitions:
Exploration of the stack — X-Powered-By, source code, comments in HTML. Identify DBMS (MySQL, PostgreSQL, SQLite) before injection
Manual checking is a single quotation rig in each parameter (GET, POST, cookie, headers). Looking for SQL errors, content changes, response delays
Type definition — error-based (error visible), UNION-based (data in response), boolean-blind (different content), time-blind (different time)
Columns – ORDER BY N with increment, then UNION SELECT NULL,... with selection of quantity
Manual extraction of 2-3 characters - confirm the format of the data and the validity of payload
Transition to sqlmap — sqlmap -r request.txt --dbms=X --technique=Y --string="Z", indicating everything I learned manually
Dump – --dbs, then -D db_name --tables, then -D db_name -T table_name --dump