Description
A login form backed by SQLite is vulnerable to classic SQL injection. The server constructs a query by directly concatenating user input into the SQL string without parameterization.
Inject SQL syntax into the username field to make the WHERE clause always true, bypassing authentication entirely.
Setup
Navigate to the challenge URL in your browser or use curl.
Submit a SQL injection payload in the username field. If you POST via curl, URL-encode the payload (' becomes %27, spaces become %20).
curl -X POST 'http://saturn.picoctf.net:<PORT_FROM_INSTANCE>/login' --data-urlencode "username=' OR 1=1-- -" --data-urlencode "password=x"# Equivalent fully-encoded URL-form body:# username=%27%20OR%201%3D1--%20-&password=xSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Understand the vulnerable queryObservationI noticed the challenge is called SQLiLite and involves a login form backed by SQLite with no parameterization, which suggested the server concatenates user input directly into the query string and that a classic tautology injection on the username field would bypass the WHERE clause entirely.The server builds: SELECT * FROM users WHERE username='INPUT' AND password='...' - injecting ' OR 1=1-- makes it always true.Learn more
The backend SQL query looks like:
SELECT * FROM users WHERE username='INPUT' AND password='PASS'When you enter
' OR 1=1-- -as the username, the query becomes:SELECT * FROM users WHERE username='' OR 1=1-- - ' AND password='...'The single quote closes the username string literal.
OR 1=1makes the WHERE clause always true (1=1 is always true). The-- -is a SQL comment that comments out the rest of the query, including the AND password check. The result: the query returns all rows, the login succeeds as the first user (often admin).Step 2
Submit the injection payloadObservationI noticed the query structure uses AND to chain the password check after the username match, which suggested that injecting ' OR 1=1-- - as the username would close the string literal, append an always-true condition, and comment out the password check in one payload.Enter ' OR 1=1-- - as the username and anything as the password, then submit the login form.bash# In browser: Username: ' OR 1=1-- - Password: anythingbashcurl -X POST 'http://saturn.picoctf.net:<PORT_FROM_INSTANCE>/login' --data-urlencode "username=' OR 1=1-- -" --data-urlencode "password=x"Expected output
Logged in! The flag is: picoCTF{L00k5_l1k3_y0u_solv3d_it_...}What didn't work first
Tried: Using curl -d instead of --data-urlencode to send the payload
With -d, the shell interprets the single quote in ' OR 1=1-- - and curl sends a malformed or incomplete body (the quote gets stripped or causes a shell parse error). The server receives a plain string like OR 1=1-- - with no leading quote, so the SQL syntax is invalid and authentication still fails. --data-urlencode tells curl to percent-encode special characters automatically, so the single quote arrives as %27 and the payload parses correctly on the server side.
Tried: Injecting admin'-- as the username to log in as a specific admin account
This payload comments out the password check for a user named admin, but only works if a row with exactly that username exists in the database. If the table uses a different admin username or no rows match, the query returns zero rows and login fails. The ' OR 1=1-- - tautology bypasses authentication regardless of what usernames exist because OR 1=1 forces the WHERE clause true for every row in the table.
Learn more
SQL injection is consistently ranked in the OWASP Top 10 as one of the most critical web application security risks. The root cause is always the same: user input is concatenated into a SQL query string instead of being passed as a bound parameter.
The safe fix is to use parameterized queries (also called prepared statements):
cursor.execute("SELECT * FROM users WHERE username=? AND password=?", (username, password))With parameterized queries, the input is treated as data, not as SQL syntax - special characters like single quotes are escaped automatically and cannot alter the query structure.
Step 3
Extract the flag from the responseObservationI noticed the server logged us in and returned content in the HTTP response body after the injection succeeded, which suggested the flag would appear directly in that page output rather than requiring any additional enumeration.The server returns the flag in the HTTP response body or page content after successful login bypass.Learn more
In SQLite, the comment syntax is
--(two dashes). MySQL also supports#. The space after--is required in some databases (PostgreSQL) and the trailing-is sometimes added to ensure the comment parses correctly. Using-- -(dash dash space dash) is a safe choice that works in most SQL dialects.Other classic bypasses include:
admin'--(log in as the admin user specifically),' OR '1'='1(without a comment, closes both sides of the string), and'/*for C-style block comments in MySQL.More advanced SQL injection techniques include UNION-based injection (appending a SELECT to extract data from other tables), blind injection (inferring data from true/false responses), and time-based blind injection (using
SLEEP()to infer data from response timing).
Interactive tools
- SQL Injection Payload GeneratorGenerate SQL injection payloads for auth bypass, UNION extraction, blind SQLi, NoSQL operator injection, and sqlmap commands. Supports MySQL, PostgreSQL, SQLite, and MSSQL.
Flag
Reveal flag
picoCTF{L00k5_l1k3_y0u_solv3d_it_...}
The flag prefix is picoCTF{L00k5_l1k3_y0u_solv3d_it_} followed by a per-instance hash suffix. Submit the SQL injection payload as the username; the flag appears in the page source after successful login.