SQL injection in CTFs: detection, exploitation, and bypasses
SQL injection is still the first web bug most players meet, and for good reason: it teaches you to think about where your input lands inside someone else's syntax. This walkthrough moves from confirming an injectable parameter, to reading data with UNION and blind techniques, to working past the filters challenge authors like to add.
What the bug actually is
An application builds a query by pasting your input into a string. When it does that without parameterised statements, your input stops being data and becomes part of the command. The whole craft of exploitation is breaking out of the data context the developer expected and into the code context they did not. Everything below is a way to confirm that break, then widen it.
Step one: detect the injection
Start by trying to break the query, not exploit it. Send a single quote and watch for a server error, a changed response length, or a different status code. If id=1' errors but id=1'' recovers, you are almost certainly inside a string literal. Confirm with a pair of payloads that should evaluate to the same and different results:
# these two should return the SAME page if injectable id=1 AND 1=1 id=1' AND '1'='1 # this one should return a DIFFERENT page (no rows / false) id=1' AND '1'='2
If the true and false cases diverge, you have a boolean oracle, which is enough for blind extraction even when nothing useful is printed. Treat any reliable difference in the response as a signal you can read, the same lens we apply when reading artifacts out of a memory image.
Step two: read data with UNION
When the page reflects query results, a UNION-based attack is the fastest path. First find the column count by incrementing an ORDER BY until it errors, then match the column types in your injected SELECT so the union is valid.
# find the column count id=1' ORDER BY 3-- - # works id=1' ORDER BY 4-- - # errors -> 3 columns # dump the version into a visible column id=-1' UNION SELECT 1,version(),3-- - # enumerate tables, then columns, then rows id=-1' UNION SELECT 1,table_name,3 FROM information_schema.tables-- -
Using id=-1 forces the original row to return empty so your injected row is what renders. From information_schema you can walk the schema until you reach the table holding the flag.
Step three: when nothing is printed (blind)
Many challenges suppress output, so you extract one bit at a time. Boolean-blind asks yes/no questions with SUBSTRING and a comparison; time-blind does the same when even the page length is constant, using a conditional sleep as the answer channel.
# boolean: is the first flag char > 'm'? id=1' AND SUBSTRING((SELECT flag FROM secrets),1,1) > 'm'-- - # time-based: sleep 3s if the condition is true id=1' AND IF(SUBSTRING((SELECT flag FROM secrets),1,1)='f',SLEEP(3),0)-- -
Blind extraction is slow by hand, so script it. Binary-search each character rather than scanning all 95 printable bytes, and serialise requests so a slow network response is never mistaken for a true time-based hit.
Step four: getting past filters
CTF authors rarely leave the injection clean. The bypasses below are the recurring ones, and each maps to a defensive failure worth naming.
- Keyword blocklist. If
SELECTis stripped once, nest it:SELSELECTECTsurvives a single naive replace. This is why blocklists are not a defence. - Whitespace filtered. Substitute comments or alternate whitespace:
UNION/**/SELECTorUNION%0aSELECT. - Quotes filtered. Build strings without quotes using hex (
0x666c6167) orCHAR(). - WAF on the parameter. Move the payload to a header or a second parameter the developer forgot to filter. The principle is to find the input the defender did not normalise.
The fix is never a cleverer filter. It is a parameterised query, where input can never re-enter the code context no matter what it contains. the lesson every SQLi challenge is quietly teaching
The transferable habit
Detection before exploitation, a reliable oracle before a loop, and a clear model of which context your bytes land in. Confirm you can tell true from false cheaply, then automate. If you cannot read the difference consistently, you do not have an injection yet, you have a guess. Practise on intentionally vulnerable targets only, and keep your notes so the next UNION takes minutes instead of an evening.
Sources
- OWASP. "SQL Injection Prevention Cheat Sheet." Read the cheat sheet
- OWASP. "Testing for SQL Injection" (Web Security Testing Guide). Read the testing guide
- MITRE. "CWE-89: Improper Neutralization of Special Elements used in an SQL Command." Read the CWE entry