Description
You've been hired by a shadowy group of pentesters who love a good puzzle. Sloppy code and legacy hashing practices left a tiny, perfect doorway for an attacker. Slip through that doorway, act as a legit user and retrieve the secret flag.
Setup
Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Find the injectable parameter
ObservationThe app has a search endpoint building queries from user input, so that parameter is where to test for injection before reaching for sqlmap.Explore the web app for search or filter functionality. The search endpoint's query parameter is vulnerable to SQL injection.Learn more
SQL injection occurs when user-supplied input is concatenated directly into a SQL query without proper sanitization or parameterization. The database engine cannot distinguish between the developer's intended SQL syntax and the attacker's injected payload, so it executes the injected commands with the full privileges of the database user running the query.
Search and filter endpoints are particularly common injection points because they often need to construct dynamic queries based on user input (e.g.,
SELECT * FROM products WHERE name LIKE '%SEARCHTERM%'). Developers sometimes apply input validation to login forms but overlook search functionality, believing it to be lower risk because it only reads data. However, SQL injection can be used for data extraction (SELECT), modification (UPDATE/INSERT/DELETE), and even operating system command execution in some configurations.Manual SQL injection testing involves trying payloads like a single quote (
'), which causes a syntax error if the input is unescaped;OR 1=1--, which makes a WHERE clause always true; andUNION SELECTstatements to append additional query results. Error messages, response time differences, and behavioral changes all indicate injection vulnerability.Step 2Run sqlmap to dump the database
ObservationManual probing confirms the injection, so hand sqlmap the fingerprinting and table dumping rather than writing UNION payloads yourself.Point sqlmap at the search endpoint and let it fingerprint and dump.--batchskips the interactive prompts so you don't have to babysit it.bash# Search is behind login, so pass your session cookie:bashsqlmap -u 'http://HOST:PORT/search?search=test' --cookie='session=...' --batch --tablesbashsqlmap -u 'http://HOST:PORT/search?search=test' --cookie='session=...' --batch -T users --dumpWhen sqlmap won't cooperate, inject by hand. sqlmap frequently fails on this challenge with
critical: unable to connect to the target URL, and some networks (university/corporate egress filtering) silently drop the obviously-malicious sqlmap traffic. If that happens, run a singleUNIONby hand from the search box instead - or paste it through the picoCTF web shell so the request originates from inside their network rather than yours.The backend is SQLite and the search query returns two columns, so your UNION must select exactly two. Close the string, add the union, and comment out the trailing SQL:
' UNION SELECT username, password FROM users--If you don't already know the table or column names, enumerate the SQLite schema first (still two columns):
' UNION SELECT name, sql FROM sqlite_master WHERE type='table'--Get the column count wrong (one column, three columns) and the query errors out instead of returning rows - that mismatch is itself the signal you're on the right track. The dumped row gives you a username and its MD5 hash; crack that next.
What didn't work first
Tried: Running sqlmap without the --cookie flag and expecting it to authenticate and reach the search endpoint.
The search endpoint sits behind the login, so without a session cookie sqlmap follows the redirect and probes the login page instead, reporting it as non-injectable or failing outright. Capture your cookie from dev tools after logging in and pass it, so sqlmap actually reaches the protected URL.
Tried: Using a UNION SELECT with three columns (username, password, email) when the backend query only returns two columns.
A UNION needs the injected SELECT to have the same column count as the original. Too many or too few and SQLite errors out with a column-count mismatch and returns nothing. Probe by adding NULL columns one at a time until rows appear, then build the extraction payload around that count.
Learn more
sqlmap is an open-source automated SQL injection tool that detects and exploits injection vulnerabilities across all major database backends (MySQL, PostgreSQL, SQLite, MSSQL, Oracle). It probes the parameter using four detection strategies: boolean-based blind (compares response diffs between always-true and always-false payloads), time-based blind (injects
SLEEP(5)-style delays and times the response), error-based (looks for SQL error reflections likeYou have an error in your SQL syntax), and UNION-based (appendsUNION SELECTto extract data inline).The
--batchflag suppresses every prompt: "Do you want sqlmap to follow this redirect?", "Skip remaining detection tests for the parameter?", the DBMS narrowing question ("It looks like the back-end DBMS is MySQL. Do you want to skip test payloads for other DBMSes?"), the risk-level prompt, and the dump confirmation.--tableslists tables;-T users --dumpextracts every row from theuserstable.Note the DBMS sqlmap reports - it matters. UNION column counts, comment syntax (
--vs#), and string concatenation (||vsCONCAT) differ between MySQL, PostgreSQL, and SQLite. If you ever drop into manual payloads, you need to know which dialect you're writing for.Step 3Crack the MD5 hash
ObservationThe dump stores the ctf-player password as an unsalted 32-character hex string, which is raw MD5. A lookup or a wordlist recovers it quickly.The ctf-player row contains a raw MD5. Try CrackStation first (instant if it's in their lookup table), then fall back to hashcat with rockyou.txt.bash# Online: paste hash at crackstation.netbashhashcat -m 0 hash.txt rockyou.txtWhat didn't work first
Tried: Passing the hash to hashcat with -m 1000 (NTLM mode) instead of -m 0 (raw MD5).
NTLM and MD5 are both 32 hex characters and indistinguishable at a glance. Mode 1000 computes NTLM, which never matches an MD5 in the database, so hashcat exhausts rockyou.txt and reports nothing even for a trivial password. The dump usually labels the column; if not, run the hash through an identifier before picking a mode.
Tried: Pasting the hash into CrackStation and stopping when it returns no result, assuming the password is uncrackable.
CrackStation covers hashes from public breach corpora only, so a CTF-specific password comes back empty even when hashcat cracks it from rockyou.txt in seconds. Use the lookup as a fast first pass and follow it with a wordlist attack and a rule set.
Learn more
MD5 was deprecated for security use around 2004 (Wang's collision paper) and OWASP has flagged it as unfit for password storage since at least 2010. Its weaknesses for password hashing are: (1) it is extremely fast - modern GPUs compute billions of MD5s per second; (2) it has no built-in salt, so identical passwords produce identical hashes, enabling precomputed rainbow tables; (3) collisions are now trivial to construct.
CrackStation vs hashcat - know when to use each. CrackStation is a precomputed lookup: it indexes ~15 billion known hash-plaintext pairs. If your password is in any leaked breach corpus, the lookup is instant and free. Use it first. If CrackStation comes back empty, switch to hashcat: GPU-accelerated wordlist + rules attack.
-m 0selects raw MD5;rockyou.txt(14 million passwords from the 2009 RockYou breach) is the standard starting wordlist. Add rules like-r best64.ruleto mutate each candidate (capitalization, common suffixes, leetspeak).Modern best practice is bcrypt, Argon2id, or scrypt - adaptive functions designed to be slow and salted. They drop GPU throughput from billions/sec to thousands/sec, making cracking infeasible for any non-trivial password. See the Hash Cracking for CTF guide for a fuller hashcat workflow.
Step 4Log in as ctf-player and read the flag
ObservationWith the plaintext password recovered, log in through the app and the authenticated area shows the flag.Use the cracked password to log into the ctf-player account. The flag is displayed after logging in.Learn more
With a cracked password, the attacker gains full authenticated access to the ctf-player account - the same access its legitimate owner would have. This is called credential-based access and is often more impactful than direct exploitation because it bypasses many secondary security controls (IP allowlists, MFA in some configurations, security monitoring alerts for unusual activity patterns).
This challenge illustrates a complete attack chain common in real penetration tests: SQL injection extracts the credential database, offline cracking recovers plaintext passwords, and those credentials enable authenticated access to administrative functions. The combination of an injection vulnerability and weak password hashing creates a two-step path to full compromise. Each vulnerability alone might be rated medium severity, but chained together they become critical.
Defense-in-depth against this attack chain requires addressing every layer: parameterized queries (prevent SQL injection), strong password hashing with bcrypt/Argon2 (prevent credential recovery even if the database is dumped), and multi-factor authentication (prevent login even with a known password). Removing any single link in the attack chain would have stopped this exploit.
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{F0uNd_s3cr3T_K3y_f0R_w3_...}
The flag is displayed after logging in as ctf-player, whose MD5 password hash is recovered via manual UNION injection (sqlmap typically fails on this challenge with 'unable to connect').
Key takeaway
How to prevent this
How to prevent this
Two independent failures chained here: injectable query and crackable hash. Either fix kills the attack.
- Use parameterized queries everywhere.
cursor.execute("SELECT * FROM products WHERE name LIKE %s", (q,))is safe; string concatenation never is. Modern ORMs (SQLAlchemy, Prisma, Hibernate) parameterize by default; just don't reach for raw SQL. - Hash passwords with
bcrypt,argon2id, orscrypt. MD5 / SHA-1 / SHA-256 are designed to be fast; password hashes need to be deliberately slow. A cost factor of ~250ms per hash makes GPU cracking infeasible. - Add MFA on admin accounts and least-privilege the database user. The web app should not have permissions to read the
userstable outside the auth flow, let aloneUNION SELECTagainst it.