手把手系列: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,保存。如图所示。
屏幕截图 2026-09-22 152821

关于难度
简单说就是难度需要循序渐进,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 里抓一次请求:

  1. 左侧 手工测试 → MITM交互式劫持 → 免配置启动(或者用你自己配好代理的浏览器)

  2. 在打开的浏览器里进 DVWA,点左侧菜单的 SQL Injection

  3. 输入 1,点 Submit

  4. 回到 Yakit 看 History,能找到类似这样一条:

GET /vulnerabilities/sqli/?id=1&Submit=Submit HTTP/1.1
Host: 10.10.10.128:8080
...

如图:
屏幕截图 2026-09-23 163248

  1. 右键 → 发送到 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 语法错误。
屏幕截图 2026-09-23 164224

看到报错,说明几件事:

① 存在注入,用户输入确实被拼进 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' 恒为真,所以页面正常返回用户信息。
屏幕截图 2026-09-23 164453

第二个 payload(假):

id=1' and '1'='2
SELECT first_name, last_name FROM users WHERE user_id = '1' and '1'='2'

'1'='2' 恒为假,所以页面返回空。
屏幕截图 2026-09-23 164528

两次结果不一样,注入就可以确认了。

为什么这里必须带引号? 因为 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#

屏幕截图 2026-09-23 164913
发现3列出现报错了,说明列数小于3,试下2呢。

id=1' order by 2#

image
发现成功返回了,说明只有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(如图所示),说明两个位置都能回显——那我们就可以把想查的东西替换到这两个位置上。
image

为什么 payload 前面要保留 id=1'?
因为原查询是 WHERE user_id = '1',如果表里没有 id 为 1 的用户,前半段会返回空,UNION 的结果就只剩我们注入的那部分。
也有人习惯写 id=-1',用个不存在的值确保前半段一定为空。两种都行。

七、第五步:开始脱库

现在按理论篇讲的那条路径一步步来,依照当前库 → 表 → 字段 → 数据的顺序顺藤摸瓜。

7.1 先拿当前数据库名

id=1' union select 1,database()#

页面上会显示库名,DVWA 默认是 dvwa。如图所示。
image

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 两张表。如图所示。
image

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 了吧——这两个就是我们真正想要的东西。如图所示。
image

7.4 最后一步:把用户名和密码拖出来

id=1' union select user,password from users#

页面上会直接把所有用户的用户名和密码哈希列出来。如图所示。
image

如果想一次看全(用户名和密码拼在同一列),可以这样(上面提过的一行列出):

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(...);

问题一目了然:

  1. $id 直接拼进了 SQL——没有任何过滤、没有任何转义
  2. 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 目标字段  → 拿数据

这八步是联合查询注入的标准套路。 你现在是「照着步骤走」,等打过几个不同的靶场之后,你会发现自己开始看到注入点就大概知道该怎么打——那时候才叫真的会了。

参考

预告一下特别篇:总以为病毒会离我很远,前一阵也是中招了,还迟钝的让病毒跑了一天才发现,借此机会分享一下纯小白首战挖矿类病毒的经验和丢脸经历。
174713644cd6f2825e8733d3199b8534

posted @ 2026-09-23 20:05  半扇排骨  阅读(28)  评论(0)    收藏  举报