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
Launch the challenge instance and open the web application.
Register for an account on the web application to access the search functionality.
Install sqlmap if not already available: pip install sqlmap
Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Find the injectable parameterObservationI noticed the web app exposed a search endpoint that constructs dynamic queries based on user input, which suggested the query parameter was a prime candidate for SQL injection testing before automating with 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 2
Run sqlmap to dump the databaseObservationI noticed manual probing confirmed SQL injection on the search parameter, which suggested using sqlmap to automate fingerprinting and table dumping rather than crafting UNION payloads by hand.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/login?search=test' --cookie='session=...' --batch --tablesbashsqlmap -u 'http://HOST:PORT/login?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 is behind login, so sqlmap without a valid session cookie receives a redirect to the login page and probes that page instead. It reports the login form as non-injectable or fails entirely with 'unable to connect to the target URL'. Pass your session cookie with --cookie='session=...' captured from browser dev tools after logging in so sqlmap actually reaches the protected search URL.
Tried: Using a UNION SELECT with three columns (username, password, email) when the backend query only returns two columns.
SQLite UNION requires the injected SELECT to have the same number of columns as the original query. Selecting too many or too few columns causes a 'SELECTs to the left and right of UNION do not have the same number of result columns' error with no data returned. Probe the column count first by adding NULL columns one at a time until rows appear, then confirm the correct count is two before crafting the data-extraction payload.
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 3
Crack the MD5 hashObservationI noticed the database dump revealed the ctf-player password stored as a 32-character hex string with no salt, which are the telltale signs of raw MD5 and suggested a lookup or wordlist attack would recover the plaintext 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 hashes are both 32 hex characters and look identical at a glance. hashcat with -m 1000 computes the NTLM digest (MD4 of the UTF-16LE password) and never matches the MD5 stored in the database, so it exhausts rockyou.txt and reports 'Exhausted' even for trivial passwords. The sqlmap dump or manual UNION output will label the column type; if it does not, run the hash through a hash-identifier tool first to confirm MD5 before choosing a mode.
Tried: Pasting the hash into CrackStation and stopping when it returns no result, assuming the password is uncrackable.
CrackStation only covers hashes that appear in publicly leaked breach corpora. A CTF-specific password or a common word not in their 15-billion-entry index returns empty even if hashcat with rockyou.txt cracks it in seconds. CrackStation is the fast first pass; a hashcat wordlist attack with rockyou.txt and a rule set like best64 should always be the follow-up when the lookup comes back empty.
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 4
Log in as ctf-player and read the flagObservationI noticed the hash cracker returned the plaintext password for ctf-player, which suggested logging in with those credentials directly through the web app to access the authenticated area where the flag is displayed.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 admin account - the same access a legitimate admin 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{sql_m4p_m4st3r_...}
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.